промежуточный коммит
This commit is contained in:
parent
23201db5ce
commit
32e65a7742
|
|
@ -32,6 +32,14 @@
|
|||
<artifactId>org.eclipse.jgit</artifactId>
|
||||
<version>7.3.0.202506031305-r</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.hierynomus/sshj -->
|
||||
<dependency>
|
||||
<groupId>com.hierynomus</groupId>
|
||||
<artifactId>sshj</artifactId>
|
||||
<version>0.39.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -1,24 +1,26 @@
|
|||
package ru.kirillius.pf.sdn;
|
||||
|
||||
import ru.kirillius.pf.sdn.External.API.GitSubscription;
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class App extends AppContext {
|
||||
private final static File configFile = new File("config.json");
|
||||
|
||||
@SneakyThrows
|
||||
public App(File configFile) {
|
||||
super(configFile);
|
||||
GitSubscription subscription = new GitSubscription(this);
|
||||
RepositoryConfig repositoryConfig = new RepositoryConfig("test", GitSubscription.class, "https://git.kirillius.ru/kirillius/docker-decompose.git");
|
||||
Map<String, NetworkResourceBundle> resources = subscription.getResources(repositoryConfig);
|
||||
|
||||
|
||||
getSubscriptionManager().triggerUpdate();
|
||||
|
||||
|
||||
}
|
||||
|
||||
static {
|
||||
|
|
@ -28,7 +30,15 @@ public class App extends AppContext {
|
|||
public static void main(String[] args) {
|
||||
|
||||
try (App app = new App(configFile)) {
|
||||
|
||||
app.getEventsHandler().getNetworkManagerUpdateEvent().add(new EventListener<NetworkResourceBundle>() {
|
||||
@Override
|
||||
public void invoke(NetworkResourceBundle bundle) throws Exception {
|
||||
SystemLogger.message("Network resource bundle updated.", CTX);
|
||||
}
|
||||
});
|
||||
while (true) {
|
||||
Thread.yield();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,20 @@ import ru.kirillius.pf.sdn.core.Context;
|
|||
import ru.kirillius.pf.sdn.core.ContextEventsHandler;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Plugin;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AppContext implements Context, Closeable {
|
||||
|
||||
protected final static String CTX = AppContext.class.getSimpleName();
|
||||
|
||||
public AppContext(File configFile) {
|
||||
try {
|
||||
config = Config.load(configFile);
|
||||
|
|
@ -34,6 +41,30 @@ public class AppContext implements Context, Closeable {
|
|||
ASInfoService.setProvider(new HEInfoProvider(this));
|
||||
networkManager = new NetworkManager(this);
|
||||
networkManager.getInputResources().add(config.getCustomResources());
|
||||
subscriptionManager = new SubscriptionManager(this);
|
||||
subscribe();
|
||||
initPlugins();
|
||||
}
|
||||
|
||||
private void initPlugins() {
|
||||
config.getEnabledPlugins().forEach(pluginClass -> {
|
||||
var plugin = Plugin.loadPlugin(pluginClass, this);
|
||||
loadedPlugins.add(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
private final List<Plugin<?>> loadedPlugins = new ArrayList<>();
|
||||
|
||||
private void subscribe() {
|
||||
var eventsHandler = getEventsHandler();
|
||||
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
||||
var manager = getNetworkManager();
|
||||
var inputResources = getNetworkManager().getInputResources();
|
||||
inputResources.clear();
|
||||
inputResources.add(config.getCustomResources());
|
||||
inputResources.add(bundle);
|
||||
manager.triggerUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -47,9 +78,18 @@ public class AppContext implements Context, Closeable {
|
|||
private final Server server;
|
||||
@Getter
|
||||
private final ASInfoService ASInfoService;
|
||||
@Getter
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
loadedPlugins.forEach(plugin -> {
|
||||
try {
|
||||
plugin.close();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error closing plugin", CTX, e);
|
||||
}
|
||||
});
|
||||
ASInfoService.close();
|
||||
networkManager.close();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
|
||||
import lombok.*;
|
||||
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.AbstractPlugin;
|
||||
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.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class FRRPlugin extends AbstractPlugin<FRRPlugin.FRRConfig> {
|
||||
private final static String SUBNET_PATTERN = "{%subnet}";
|
||||
private final static String GW_PATTERN = "{%gateway}";
|
||||
private final static String CTX = FRRPlugin.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
public FRRPlugin(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
||||
}
|
||||
|
||||
private void updateSubnets(List<IPv4Subnet> subnets) {
|
||||
for (var entry : config.instances) {
|
||||
SystemLogger.message("Updating subnets in FRR " + entry.shellConfig.toString(), CTX);
|
||||
try (var shell = new ShellExecutor(entry.shellConfig)) {
|
||||
SystemLogger.message("Fetching existing subnets...", CTX);
|
||||
var existingConfig = executeVTYCommand(new String[]{"show running"}, shell);
|
||||
|
||||
var patternOffset = entry.subnetPattern.indexOf(SUBNET_PATTERN);
|
||||
if (patternOffset == -1) {
|
||||
SystemLogger.error("Unable to parse FRR config '" + entry.subnetPattern + "' because " + SUBNET_PATTERN + " is not found", CTX);
|
||||
continue;
|
||||
}
|
||||
var startPattern = entry.subnetPattern.substring(0, patternOffset);
|
||||
var endPattern = entry.subnetPattern.substring(patternOffset + SUBNET_PATTERN.length()).replaceAll(Pattern.quote(GW_PATTERN), entry.gateway);
|
||||
|
||||
|
||||
var existingSubnets = new ArrayList<IPv4Subnet>();
|
||||
for (var line : existingConfig.split(Pattern.quote("\n"))) {
|
||||
line = line.trim();
|
||||
if (line.startsWith(startPattern)) {
|
||||
var subnetString = line.substring(startPattern.length()).replaceAll(Pattern.quote(endPattern), "").trim();
|
||||
|
||||
var subnet = new IPv4Subnet(subnetString);
|
||||
existingSubnets.add(subnet);
|
||||
}
|
||||
}
|
||||
|
||||
SystemLogger.message("There is " + existingSubnets.size() + " managed subnets", CTX);
|
||||
|
||||
|
||||
//удаляем лишние подсети
|
||||
var commandsToRemove = existingSubnets.stream()
|
||||
.filter(subnet -> !subnets.contains(subnet) && !entry.essentialSubnets.contains(subnet))
|
||||
.map(subnet -> "no " + entry.subnetPattern
|
||||
.replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
|
||||
.replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
|
||||
.toList();
|
||||
|
||||
if (!commandsToRemove.isEmpty()) {
|
||||
SystemLogger.message( commandsToRemove.size() + " subnets should be removed", CTX);
|
||||
executeVTYCommandBundle(commandsToRemove, true, shell, count -> SystemLogger.message(count + "/" + commandsToRemove.size() + " subnets has been removed", CTX));
|
||||
}
|
||||
|
||||
|
||||
//добавляем новые подсети
|
||||
var commandsToAdd = subnets.stream().filter(subnet -> !existingSubnets.contains(subnet))
|
||||
.map(subnet -> entry.subnetPattern
|
||||
.replaceAll(Pattern.quote(GW_PATTERN), entry.gateway)
|
||||
.replaceAll(Pattern.quote(SUBNET_PATTERN), subnet.toString()))
|
||||
.toList();
|
||||
if (!commandsToAdd.isEmpty()) {
|
||||
SystemLogger.message( commandsToAdd.size() + " subnets should be added", CTX);
|
||||
executeVTYCommandBundle(commandsToAdd, true, shell, count -> SystemLogger.message(count + "/" + commandsToAdd.size() + " subnets has been added", CTX));
|
||||
}
|
||||
|
||||
|
||||
SystemLogger.message("FRR update is complete", CTX);
|
||||
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Shell error", CTX, e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void executeVTYCommandBundle(List<String> commands, boolean configMode, ShellExecutor shell, Consumer<Integer> progressCallback) {
|
||||
|
||||
var buffer = new ArrayList<String>();
|
||||
if (configMode) {
|
||||
buffer.add("conf t");
|
||||
}
|
||||
for (var i = 0; i < commands.size(); i++) {
|
||||
buffer.add(commands.get(i));
|
||||
if (i % 100 == 0 && i > 0) {
|
||||
executeVTYCommand(buffer.toArray(new String[0]), shell);
|
||||
if (progressCallback != null) {
|
||||
progressCallback.accept(i);
|
||||
}
|
||||
buffer.clear();
|
||||
if (configMode) {
|
||||
buffer.add("conf t");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
executeVTYCommand(buffer.toArray(new String[0]), shell);
|
||||
if (progressCallback != null) {
|
||||
progressCallback.accept(commands.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String executeVTYCommand(String[] command, ShellExecutor shell) {
|
||||
var buffer = new ArrayList<String>();
|
||||
buffer.add("vtysh");
|
||||
for (var part : command) {
|
||||
buffer.add("-c");
|
||||
buffer.add(part);
|
||||
}
|
||||
|
||||
return shell.executeCommand(buffer.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||
}
|
||||
|
||||
@JSONSerializable
|
||||
public static class FRRConfig {
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONArrayProperty(type = Entry.class)
|
||||
private List<Entry> instances = new ArrayList<>();
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable
|
||||
public static class Entry {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private ShellExecutor.Config shellConfig = new ShellExecutor.Config();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String subnetPattern = "ip route {%subnet} {%gateway} 100";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String gateway = "127.0.0.1";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONArrayProperty(type = IPv4Subnet.class)
|
||||
private List<IPv4Subnet> essentialSubnets = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ package ru.kirillius.pf.sdn.External.API;
|
|||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||
|
|
@ -11,9 +14,9 @@ import ru.kirillius.pf.sdn.core.Util.HashUtil;
|
|||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.*;
|
||||
|
||||
public class GitSubscription implements SubscriptionProvider {
|
||||
private final Context context;
|
||||
|
|
@ -39,10 +42,27 @@ public class GitSubscription implements SubscriptionProvider {
|
|||
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
|
||||
checkAndPullUpdates(repository);
|
||||
}
|
||||
|
||||
|
||||
repository.close();
|
||||
return Map.of();
|
||||
|
||||
var resourcesDir = new File(repoDir, "resources");
|
||||
if (!resourcesDir.exists()) {
|
||||
SystemLogger.error(resourcesDir + " is not exist in repo (" + config.getSource() + ")", CTX);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
var map = new HashMap<String, NetworkResourceBundle>();
|
||||
for (var file : Objects.requireNonNull(resourcesDir.listFiles())) {
|
||||
try (var stream = new FileInputStream(file)) {
|
||||
var name = file.getName();
|
||||
if(!name.endsWith(".json")) {
|
||||
continue;
|
||||
}
|
||||
var bundle = JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), NetworkResourceBundle.class);
|
||||
map.put(name.substring(0, name.length() - 5), bundle);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
Есть альтернативы:
|
||||
|
||||
Существует несколько API для получения префиксов (IP-блоков) по номеру ASN. Вот основные варианты:
|
||||
|
||||
## 1. **RIPE Stat (RESTful API)**
|
||||
**URL:** `https://stat.ripe.net/data/announced-prefixes/data.json`
|
||||
**Пример запроса:**
|
||||
```bash
|
||||
curl "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS3333"
|
||||
```
|
||||
|
||||
## 2. **BGPView API**
|
||||
**URL:** `https://api.bgpview.io/asn/ASN/prefixes`
|
||||
**Пример:**
|
||||
```bash
|
||||
curl "https://api.bgpview.io/asn/AS3333/prefixes"
|
||||
```
|
||||
|
||||
## 3. **IPtoASN API**
|
||||
**URL:** `https://api.iptoasn.com/v1/as/ip/ASN`
|
||||
**Пример:**
|
||||
```bash
|
||||
curl "https://api.iptoasn.com/v1/as/ip/AS3333"
|
||||
```
|
||||
|
||||
## 5. **Cloudflare Radar API**
|
||||
**URL:** `https://api.cloudflare.com/client/v4/radar/asn/prefixes`
|
||||
**Пример:**
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"https://api.cloudflare.com/client/v4/radar/asn/prefixes?asn=3333"
|
||||
```
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
|
||||
import lombok.*;
|
||||
import net.schmizz.sshj.SSHClient;
|
||||
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.schmizz.sshj.common.IOUtils.readFully;
|
||||
|
||||
public class ShellExecutor implements Closeable {
|
||||
private final static String CTX = ShellExecutor.class.getSimpleName();
|
||||
private final Config config;
|
||||
private SSHClient sshClient;
|
||||
|
||||
public ShellExecutor(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public String executeCommand(String[] command) {
|
||||
var buffer = new StringJoiner(" ");
|
||||
Arrays.stream(command).forEach(e -> buffer.add('"' + e + '"'));
|
||||
|
||||
if (config.useSSH) {
|
||||
|
||||
try {
|
||||
if (sshClient == null) {
|
||||
sshClient = new SSHClient();
|
||||
sshClient.addHostKeyVerifier(new PromiscuousVerifier());
|
||||
|
||||
sshClient.connect(config.host, config.port);
|
||||
sshClient.authPassword(config.username, config.password);
|
||||
}
|
||||
|
||||
try (var session = sshClient.startSession()) {
|
||||
var process = session.exec(buffer.toString());
|
||||
var output = readFully(process.getInputStream()).toString();
|
||||
process.join(10, TimeUnit.SECONDS);
|
||||
return output;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Failed to execute remote command " + buffer + " via ssh on host " + config.host, CTX, e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
|
||||
try {
|
||||
var runtime = Runtime.getRuntime();
|
||||
var process = runtime.exec(command);
|
||||
var output = readFully(process.getInputStream()).toString();
|
||||
process.waitFor();
|
||||
return output;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
SystemLogger.error("Failed to execute local shell command " + buffer, CTX, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
|
||||
|
||||
if (sshClient != null) {
|
||||
sshClient.disconnect();
|
||||
sshClient.close();
|
||||
sshClient = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JSONSerializable
|
||||
public static class Config {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private boolean useSSH = false;
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String host = "127.0.0.1";
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private int port = 22;
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String username = "root";
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String password = "securepassword";
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
var builder = new StringBuilder();
|
||||
builder.append("Shell in ");
|
||||
if (useSSH) {
|
||||
builder.append("ssh ").append(username).append("@").append(host).append(":").append(port);
|
||||
} else {
|
||||
builder.append("localhost");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class TechnitiumDNSAPI implements Closeable {
|
||||
private final String server;
|
||||
private final String authToken;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public TechnitiumDNSAPI(String server, String authToken) {
|
||||
this.server = server;
|
||||
httpClient = HttpClient.newBuilder().build();
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
private JSONObject getRequest(String api, Map<String, String> additionalParams) {
|
||||
var params = new HashMap<>(additionalParams);
|
||||
var joiner = new StringJoiner("&");
|
||||
params.put("token", authToken);
|
||||
params.forEach((key, value) -> joiner.add(key + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8)));
|
||||
var url = server + api;
|
||||
if (!params.isEmpty()) {
|
||||
url += "?" + joiner;
|
||||
}
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Accept", "application/json").GET().build();
|
||||
|
||||
try {
|
||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
var result = new JSONObject(response.body());
|
||||
if (result.getString("status").equals("ok")) {
|
||||
return result.getJSONObject("response");
|
||||
} else {
|
||||
throw new RuntimeException("API error: " + result.getString("errorMessage"));
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("API HTTP error: " + response.statusCode());
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException("API internal error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public enum ZoneType {
|
||||
Primary, Secondary, Stub, Forwarder, SecondaryForwarder, Catalog, SecondaryCatalog
|
||||
}
|
||||
|
||||
@JSONSerializable
|
||||
public static class ZoneResponse {
|
||||
@JSONProperty
|
||||
@Getter
|
||||
private String name;
|
||||
@JSONProperty
|
||||
@Getter
|
||||
private ZoneType type;
|
||||
@JSONProperty(required = false)
|
||||
@Getter
|
||||
private String dnssecStatus;
|
||||
@JSONProperty
|
||||
@Getter
|
||||
private long soaSerial;
|
||||
@JSONProperty(required = false)
|
||||
@Getter
|
||||
private Date expiry;
|
||||
@JSONProperty(required = false)
|
||||
@Getter
|
||||
private boolean isExpired;
|
||||
@JSONProperty(required = false)
|
||||
@Getter
|
||||
private boolean syncFailed;
|
||||
@JSONProperty
|
||||
@Getter
|
||||
private boolean disabled;
|
||||
@JSONProperty
|
||||
@Getter
|
||||
private Date lastModified;
|
||||
}
|
||||
|
||||
public List<ZoneResponse> getZones() {
|
||||
var request = getRequest("/api/zones/list", Collections.emptyMap());
|
||||
return JSONUtility.deserializeCollection(request.getJSONArray("zones"), ZoneResponse.class, null).toList();
|
||||
}
|
||||
|
||||
public void createForwarderZone(String zoneName, String forwarder) {
|
||||
var params = new HashMap<String, String>();
|
||||
params.put("zone", zoneName);
|
||||
params.put("type", ZoneType.Forwarder.name());
|
||||
params.put("forwarder", forwarder);
|
||||
getRequest("/api/zones/create", params);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void deleteZone(String zoneName) {
|
||||
var params = new HashMap<String, String>();
|
||||
params.put("zone", zoneName);
|
||||
getRequest("/api/zones/delete", params);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
httpClient.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
|
||||
import lombok.*;
|
||||
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.AbstractPlugin;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class TechnitiumPlugin extends AbstractPlugin<TechnitiumPlugin.TechnitiumConfig> {
|
||||
|
||||
private final static String CTX = TechnitiumPlugin.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
public TechnitiumPlugin(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
|
||||
}
|
||||
|
||||
private void updateSubnets(List<String> domains) {
|
||||
for (var instance : config.instances) {
|
||||
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
|
||||
try (var api = new TechnitiumDNSAPI(instance.server, instance.token)) {
|
||||
var existingForwardZones = api.getZones().stream()
|
||||
.filter(zoneResponse -> zoneResponse.getType() == TechnitiumDNSAPI.ZoneType.Forwarder)
|
||||
.map(TechnitiumDNSAPI.ZoneResponse::getName)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
existingForwardZones.forEach(zoneName -> {
|
||||
if (!domains.contains(zoneName)) {
|
||||
//delete zone
|
||||
SystemLogger.message("Deleting zone " + zoneName, CTX);
|
||||
api.deleteZone(zoneName);
|
||||
}
|
||||
});
|
||||
|
||||
domains.forEach(zoneName -> {
|
||||
if (!existingForwardZones.contains(zoneName)) {
|
||||
SystemLogger.message("Creating FWD zone " + zoneName, CTX);
|
||||
api.createForwarderZone(zoneName, instance.forwarder);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error happened on DNS server " + instance.server + " sync", CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().remove(subscription);
|
||||
|
||||
}
|
||||
|
||||
@JSONSerializable
|
||||
public static class TechnitiumConfig {
|
||||
|
||||
@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 forwarder = "127.0.0.1";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String server = "http://127.0.0.1:5380";
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String token = "notoken";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
public abstract class AbstractPlugin<CT> implements Plugin<CT> {
|
||||
protected final CT config;
|
||||
protected final Context context;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public AbstractPlugin(Context context) {
|
||||
config = (CT) context.getConfig().getPluginsConfig().getConfig((Class) getClass());
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,13 @@ import lombok.NoArgsConstructor;
|
|||
import lombok.Setter;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONArrayProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.*;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
|
@ -43,6 +41,22 @@ public class Config {
|
|||
@JSONArrayProperty(type = RepositoryConfig.class)
|
||||
private List<RepositoryConfig> subscriptions = Collections.emptyList();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONArrayProperty(type = String.class)
|
||||
private List<String> subscribedResources = Collections.emptyList();
|
||||
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONProperty(required = false)
|
||||
private PluginConfigStorage pluginsConfig = new PluginConfigStorage();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONArrayProperty(type = Class.class, required = false)
|
||||
private List<Class<? extends Plugin<?>>> enabledPlugins = new ArrayList<>();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONProperty
|
||||
|
|
@ -66,7 +80,7 @@ public class Config {
|
|||
@Setter
|
||||
@Getter
|
||||
@JSONProperty
|
||||
private int mergeSubnetsCount = 10;
|
||||
private int mergeSubnetsWithUsage = 51;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import org.eclipse.jetty.server.Server;
|
|||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
|
||||
public interface Context {
|
||||
Config getConfig();
|
||||
|
|
@ -16,4 +17,5 @@ public interface Context {
|
|||
|
||||
NetworkManager getNetworkManager();
|
||||
ContextEventsHandler getEventsHandler();
|
||||
SubscriptionManager getSubscriptionManager();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import lombok.Getter;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.JSONSerializer;
|
||||
import ru.kirillius.json.SerializationException;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
|
|
@ -8,7 +9,7 @@ import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
|||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
@JSONSerializable(IPv4Subnet.Serializer.class)
|
||||
public class IPv4Subnet {
|
||||
|
||||
public final static class Serializer implements JSONSerializer<IPv4Subnet> {
|
||||
|
|
@ -24,7 +25,8 @@ public class IPv4Subnet {
|
|||
}
|
||||
}
|
||||
|
||||
private final long address;
|
||||
@Getter
|
||||
private final long longAddress;
|
||||
@Getter
|
||||
private final int prefixLength;
|
||||
|
||||
|
|
@ -33,12 +35,12 @@ public class IPv4Subnet {
|
|||
public boolean equals(Object o) {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IPv4Subnet that = (IPv4Subnet) o;
|
||||
return address == that.address && prefixLength == that.prefixLength;
|
||||
return longAddress == that.longAddress && prefixLength == that.prefixLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(address, prefixLength);
|
||||
return Objects.hash(longAddress, prefixLength);
|
||||
}
|
||||
|
||||
public IPv4Subnet(String subnet) {
|
||||
|
|
@ -49,20 +51,28 @@ public class IPv4Subnet {
|
|||
var prefix = Integer.parseInt(split[1]);
|
||||
IPv4Util.validatePrefix(prefix);
|
||||
|
||||
address = IPv4Util.ipAddressToLong(split[0]);
|
||||
longAddress = IPv4Util.ipAddressToLong(split[0]);
|
||||
prefixLength = prefix;
|
||||
}
|
||||
|
||||
public IPv4Subnet(long longAddress, int prefixLength) {
|
||||
this.longAddress = longAddress;
|
||||
this.prefixLength = prefixLength;
|
||||
}
|
||||
|
||||
public IPv4Subnet(String address, int prefixLength) {
|
||||
IPv4Util.validatePrefix(prefixLength);
|
||||
|
||||
this.address = IPv4Util.ipAddressToLong(address);
|
||||
this.longAddress = IPv4Util.ipAddressToLong(address);
|
||||
this.prefixLength = prefixLength;
|
||||
}
|
||||
|
||||
public long count() {
|
||||
return IPv4Util.calculateCountForPrefixLength(prefixLength);
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return IPv4Util.longToIpAddress(address);
|
||||
return IPv4Util.longToIpAddress(longAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -77,7 +87,7 @@ public class IPv4Subnet {
|
|||
return false; //can't overlap larger prefix
|
||||
}
|
||||
|
||||
return (address & commonMask) == (subnet.address & commonMask);
|
||||
return (longAddress & commonMask) == (subnet.longAddress & commonMask);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package ru.kirillius.pf.sdn.core.Networking;
|
|||
|
||||
import lombok.Getter;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
|
@ -12,8 +14,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
|
||||
public class NetworkManager implements Closeable {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private Context context;
|
||||
private final static String CTX = NetworkManager.class.getSimpleName();
|
||||
private final Context context;
|
||||
|
||||
public NetworkManager(Context context) {
|
||||
this.context = context;
|
||||
|
|
@ -33,28 +35,43 @@ public class NetworkManager implements Closeable {
|
|||
|
||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||
|
||||
public synchronized void triggerUpdate() {
|
||||
public void triggerUpdate() {
|
||||
if (isUpdatingNow()) {
|
||||
return;
|
||||
}
|
||||
SystemLogger.message("Updating network manager", CTX);
|
||||
|
||||
updateProcess.set(executor.submit(() -> {
|
||||
SystemLogger.message("Update is started", CTX);
|
||||
var config = context.getConfig();
|
||||
var filteredResources = config.getFilteredResources();
|
||||
var asn = new ArrayList<>(inputResources.getASN());
|
||||
asn.removeAll(filteredResources.getASN());
|
||||
|
||||
|
||||
if (cacheInvalid.get()) {
|
||||
fetchPrefixes(asn);
|
||||
}
|
||||
|
||||
var subnets = new HashSet<>(inputResources.getSubnets());
|
||||
prefixCache.values().forEach(subnets::addAll);
|
||||
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());
|
||||
|
||||
SystemLogger.message(merged.getMergedSubnets().size() + " subnets has been merged to " + merged.getResult().size() + " new subnets", CTX);
|
||||
|
||||
var domains = new HashSet<>(inputResources.getDomains());
|
||||
filteredResources.getDomains().forEach(domains::remove);
|
||||
//check overlaps
|
||||
//check domain overlaps
|
||||
|
||||
var domainsToRemove = new HashSet<String>();
|
||||
for (var domainToMatch : domains) {
|
||||
|
|
@ -69,9 +86,11 @@ public class NetworkManager implements Closeable {
|
|||
domains.removeAll(domainsToRemove);
|
||||
|
||||
outputResources.setASN(Collections.unmodifiableList(asn));
|
||||
outputResources.setSubnets(subnets.stream().toList());
|
||||
outputResources.setSubnets(merged.getResult());
|
||||
outputResources.setDomains(domains.stream().toList());
|
||||
|
||||
SystemLogger.message("Update is complete", CTX);
|
||||
|
||||
try {
|
||||
context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources);
|
||||
} catch (Exception e) {
|
||||
|
|
@ -80,12 +99,14 @@ public class NetworkManager implements Closeable {
|
|||
}));
|
||||
}
|
||||
|
||||
|
||||
public void invalidateCache() {
|
||||
cacheInvalid.set(true);
|
||||
}
|
||||
|
||||
private void fetchPrefixes(List<Integer> systems) {
|
||||
systems.forEach(as -> {
|
||||
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
|
||||
var service = context.getASInfoService();
|
||||
var future = service.getPrefixes(as);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn.core.Networking;
|
|||
|
||||
import lombok.*;
|
||||
import ru.kirillius.json.JSONArrayProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -12,6 +13,10 @@ import java.util.List;
|
|||
@Builder
|
||||
@JSONSerializable
|
||||
public class NetworkResourceBundle {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty(required = false)
|
||||
private String description = "";
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONArrayProperty(type = Integer.class)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public interface Plugin<CT> extends Closeable {
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Class<T> getConfigClass(Class<? extends Plugin<T>> pluginClass) {
|
||||
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
|
||||
var typeArguments = genericSuperclass.getActualTypeArguments();
|
||||
return (Class<T>) typeArguments[0];
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
static <T extends Plugin<?>> T loadPlugin(Class<T> pluginClass, Context context) {
|
||||
return pluginClass.getConstructor(Context.class).newInstance(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.JSONSerializer;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.SerializationException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@NoArgsConstructor
|
||||
@JSONSerializable(PluginConfigStorage.Serializer.class)
|
||||
public class PluginConfigStorage {
|
||||
|
||||
public final static class Serializer implements JSONSerializer<PluginConfigStorage> {
|
||||
|
||||
@Override
|
||||
public Object serialize(PluginConfigStorage pluginConfigStorage) throws SerializationException {
|
||||
var json = new JSONObject();
|
||||
pluginConfigStorage.configs.forEach((key, value) -> {
|
||||
json.put(key.getName(), JSONUtility.serializeStructure(value));
|
||||
});
|
||||
return json;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Override
|
||||
public PluginConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
|
||||
var loader = getClass().getClassLoader();
|
||||
var json = (JSONObject) o;
|
||||
var storage = new PluginConfigStorage();
|
||||
json.keySet().forEach(key -> {
|
||||
try {
|
||||
var pluginClass = loader.loadClass(key);
|
||||
var value = json.getJSONObject(key);
|
||||
var configClass = Plugin.getConfigClass((Class) pluginClass);
|
||||
storage.configs.put((Class)pluginClass, JSONUtility.deserializeStructure(value, configClass));
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Class<? extends Plugin<?>>, Object> configs = new ConcurrentHashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SneakyThrows
|
||||
public <CT> CT getConfig(Class<? extends Plugin<CT>> pluginClass) {
|
||||
if (!configs.containsKey(pluginClass)) {
|
||||
var configClass = Plugin.getConfigClass(pluginClass);
|
||||
var instance = configClass.getConstructor().newInstance();
|
||||
configs.put(pluginClass, instance);
|
||||
}
|
||||
return (CT) configs.get(pluginClass);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
|||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
|
@ -14,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
public class SubscriptionManager implements Closeable {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
|
||||
private final Map<Class<? extends SubscriptionProvider>, SubscriptionProvider> providerCache = new ConcurrentHashMap<>();
|
||||
|
||||
public SubscriptionManager(Context context) {
|
||||
this.context = context;
|
||||
|
|
@ -38,13 +42,35 @@ public class SubscriptionManager implements Closeable {
|
|||
updateProcess.set(executor.submit(() -> {
|
||||
var bundle = new NetworkResourceBundle();
|
||||
|
||||
var config = context.getConfig();
|
||||
var subscribedResources = config.getSubscribedResources();
|
||||
for (var repoConfig : config.getSubscriptions()) {
|
||||
var providerType = repoConfig.getType();
|
||||
var provider = providerCache.get(providerType);
|
||||
if (provider == null) {
|
||||
//noinspection unchecked
|
||||
provider = SubscriptionProvider.instantiate((Class<? extends SubscriptionProvider>) providerType, context);
|
||||
//noinspection unchecked
|
||||
providerCache.put((Class<? extends SubscriptionProvider>) providerType, provider);
|
||||
}
|
||||
var resources = provider.getResources(repoConfig);
|
||||
|
||||
resources.keySet().forEach(key -> {
|
||||
var resourceName = repoConfig.getName() + ":" + key;
|
||||
//добавляем только выбранные ресурсы
|
||||
if (subscribedResources.contains(resourceName)) {
|
||||
bundle.add(resources.get(key));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
outputResources.clear();
|
||||
outputResources.add(bundle);
|
||||
try {
|
||||
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new RuntimeException(e); //FIXME to LOG
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package ru.kirillius.pf.sdn.core.Util;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class IPv4Util {
|
||||
|
|
@ -50,4 +56,219 @@ public class IPv4Util {
|
|||
}
|
||||
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
|
||||
}
|
||||
|
||||
public interface SummarisationResult {
|
||||
List<IPv4Subnet> getResult();
|
||||
|
||||
Set<IPv4Subnet> getMergedSubnets();
|
||||
}
|
||||
|
||||
private static class SubnetSummaryUtility implements SummarisationResult {
|
||||
|
||||
@Getter
|
||||
private final List<IPv4Subnet> result;
|
||||
private final Collection<IPv4Subnet> source;
|
||||
@Getter
|
||||
private final Set<IPv4Subnet> mergedSubnets = new HashSet<>();
|
||||
|
||||
|
||||
public SubnetSummaryUtility(Collection<IPv4Subnet> subnets, int usePercentage) {
|
||||
source = subnets;
|
||||
result = new ArrayList<>(subnets);
|
||||
summaryOverlapped();
|
||||
mergeNeighbours();
|
||||
summaryWithUsage(usePercentage > 50 ? usePercentage : 51);
|
||||
summaryOverlapped();
|
||||
result.sort(Comparator.comparing(IPv4Subnet::getLongAddress));
|
||||
}
|
||||
|
||||
private void summaryOverlapped() {
|
||||
if (result.size() < 2) {
|
||||
return;
|
||||
}
|
||||
//check subnets overlaps
|
||||
var overlapped = new ArrayList<IPv4Subnet>();
|
||||
var orderedByPrefix = result.stream().sorted(Comparator.comparing(IPv4Subnet::getPrefixLength)).toList();
|
||||
orderedByPrefix.stream()
|
||||
.filter(subnet -> subnet.getPrefixLength() > 32)
|
||||
.forEach(parent -> orderedByPrefix.forEach(subnet -> {
|
||||
if (subnet.equals(parent)) {
|
||||
return;
|
||||
}
|
||||
if (parent.overlaps(subnet)) {
|
||||
overlapped.add(subnet);
|
||||
if(source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
}
|
||||
}));
|
||||
overlapped.forEach(result::remove);
|
||||
}
|
||||
|
||||
private void mergeNeighbours() {
|
||||
if (result.size() < 2) {
|
||||
return;
|
||||
}
|
||||
var availableLengths = result.stream().map(IPv4Subnet::getPrefixLength).collect(Collectors.toSet());
|
||||
for (var length = 32; length > 0; length--) {
|
||||
if (!availableLengths.contains(length)) {
|
||||
continue;
|
||||
}
|
||||
var finalLength = length;
|
||||
var largerPrefixLength = length - 1;
|
||||
var largerPrefixMask = IPv4Util.calculateMask(largerPrefixLength);
|
||||
|
||||
|
||||
var selectedSubnets = result.stream().filter(subnet -> subnet.getPrefixLength() == finalLength).sorted(Comparator.comparing(IPv4Subnet::getLongAddress)).toList();
|
||||
//проверяем является ли адрес подсети таким же как адрес подсети с перфиксом -1
|
||||
for (var i = 0; i < selectedSubnets.size() - 1; i++) {
|
||||
var subnet = selectedSubnets.get(i);
|
||||
var next = selectedSubnets.get(i + 1);
|
||||
var firstAddress = subnet.getLongAddress();
|
||||
if (firstAddress != (firstAddress & largerPrefixMask)) {
|
||||
continue;
|
||||
}
|
||||
var largerSubnet = new IPv4Subnet(subnet.getAddress(), largerPrefixLength);
|
||||
if (largerSubnet.overlaps(next)) {
|
||||
//если подсеть перекрывает соседнюю, то удаляем обе и добавляем новую
|
||||
availableLengths.add(largerPrefixLength);
|
||||
result.remove(subnet);
|
||||
result.remove(next);
|
||||
result.add(largerSubnet);
|
||||
if(source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
if(source.contains(next)) {
|
||||
mergedSubnets.add(next);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int findMinPrefixLength() {
|
||||
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).min().getAsInt();
|
||||
}
|
||||
|
||||
private int findMaxPrefixLength() {
|
||||
return result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
|
||||
}
|
||||
|
||||
private long findMinAddress() {
|
||||
return result.stream().mapToLong(IPv4Subnet::getLongAddress).min().getAsLong();
|
||||
}
|
||||
|
||||
private long findMaxAddress() {
|
||||
return result.stream().mapToLong(IPv4Subnet::getLongAddress).max().getAsLong();
|
||||
}
|
||||
|
||||
private List<IPv4Subnet> findMergeCandidatesForPrefixLength(int prefixLength) {
|
||||
//создаём подсети-кандидаты, которые покроют наш список
|
||||
var maxAddress = findMaxAddress();
|
||||
|
||||
var mask = IPv4Util.calculateMask(prefixLength);
|
||||
var firstAddress = (findMinAddress() & mask);
|
||||
var lastAddress = (maxAddress & mask);
|
||||
|
||||
var candidates = new ArrayList<IPv4Subnet>();
|
||||
var candidateAddress = firstAddress;
|
||||
do {
|
||||
var candidate = new IPv4Subnet(candidateAddress, prefixLength);
|
||||
candidates.add(candidate);
|
||||
if (candidates.size() > result.size()) {
|
||||
throw new IllegalStateException("Too many IPv4 addresses when trying to summary " + result.size() + " subnets");
|
||||
}
|
||||
//поиск следующего адреса кандидата
|
||||
var nextAddress = candidateAddress + candidate.count();
|
||||
var nextSubnet = result.stream().filter(subnet -> {
|
||||
var address = subnet.getLongAddress();
|
||||
return subnet.getPrefixLength() > prefixLength && address >= nextAddress && address <= maxAddress;
|
||||
}).min(Comparator.comparingLong(IPv4Subnet::getLongAddress)).stream().findFirst();
|
||||
if (nextSubnet.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
candidateAddress = IPv4Util.createSubnetOverlapping(nextSubnet.get().getLongAddress(), prefixLength).getLongAddress();
|
||||
} while (candidateAddress <= lastAddress);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private boolean testCandidate(IPv4Subnet candidate, int usePercentage) {
|
||||
if (result.contains(candidate)) {
|
||||
return false;
|
||||
}
|
||||
var min = candidate.getLongAddress();
|
||||
var max = candidate.getLongAddress() + candidate.count() - 1;
|
||||
var overlapped = new ArrayList<IPv4Subnet>();
|
||||
var used = new AtomicLong(0L);
|
||||
result.forEach(child -> {
|
||||
if (child.getLongAddress() < min || child.getLongAddress() > max) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (candidate.overlaps(child)) {
|
||||
overlapped.add(child);
|
||||
used.addAndGet(child.count());
|
||||
}
|
||||
});
|
||||
|
||||
if (100.0 * used.get() / candidate.count() >= usePercentage) {
|
||||
//подсеть подходит под критерий
|
||||
overlapped.forEach(subnet -> {
|
||||
if(source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
});
|
||||
result.removeAll(overlapped);
|
||||
result.add(candidate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void summaryWithUsage(int usePercentage) {
|
||||
if (result.isEmpty() || usePercentage >= 100 || usePercentage <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var found = new AtomicBoolean();
|
||||
do {
|
||||
found.set(false);
|
||||
if (result.size() < 2) {
|
||||
break;
|
||||
}
|
||||
var prefixMin = findMinPrefixLength();
|
||||
var prefixMax = findMaxPrefixLength();
|
||||
for (var testPrefixLength = prefixMin - 1; testPrefixLength < prefixMax; testPrefixLength++) {
|
||||
//создаём подсети-кандидаты, которые покроют наш список
|
||||
var candidates = findMergeCandidatesForPrefixLength(testPrefixLength);
|
||||
|
||||
candidates.forEach(candidate -> {
|
||||
if (testCandidate(candidate, usePercentage)) {
|
||||
found.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (candidates.stream().anyMatch(result::contains)) {
|
||||
//если был добавлен хотя бы 1 кандидат, то нужно пересчитать maxPrefix
|
||||
prefixMax = result.stream().mapToInt(IPv4Subnet::getPrefixLength).max().getAsInt();
|
||||
}
|
||||
}
|
||||
} while (found.get());
|
||||
}
|
||||
}
|
||||
|
||||
public static SummarisationResult summarySubnets(Collection<IPv4Subnet> subnets, int usePercentage) {
|
||||
return new SubnetSummaryUtility(subnets, usePercentage);
|
||||
}
|
||||
|
||||
private static IPv4Subnet createSubnetOverlapping(long address, int prefixLength) {
|
||||
return new IPv4Subnet(address & IPv4Util.calculateMask(prefixLength), prefixLength);
|
||||
}
|
||||
|
||||
public static long calculateCountForPrefixLength(long prefixLength) {
|
||||
return 1L << (32L - prefixLength);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package ru.kirillius.pf.sdn.core.Util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class IPv4UtilTest {
|
||||
|
||||
@Test
|
||||
void summarySubnets() {
|
||||
var subnets = new ArrayList<IPv4Subnet>();
|
||||
|
||||
|
||||
for (var i = 0; i <= 254; i++) {
|
||||
subnets.add(new IPv4Subnet("192.168." + i + ".0", 24));
|
||||
}
|
||||
subnets.remove(0);
|
||||
for (var i = 1; i <= 255; i++) {
|
||||
subnets.add(new IPv4Subnet("192.168.255." + i, 32));
|
||||
}
|
||||
|
||||
for (var i = 0; i <= 254/3; i++) {
|
||||
subnets.add(new IPv4Subnet("192.169." + i + ".0", 24));
|
||||
}
|
||||
|
||||
subnets.add(new IPv4Subnet("1.1.1.1/32"));
|
||||
subnets.add(new IPv4Subnet("200.1.1.1/32"));
|
||||
|
||||
|
||||
|
||||
//subnets.forEach(System.out::println);
|
||||
|
||||
var merged = IPv4Util.summarySubnets(subnets, 51).getResult();
|
||||
|
||||
merged.forEach(System.out::println);
|
||||
|
||||
assertThat(merged).isNotNull();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue