промежуточный коммит
This commit is contained in:
parent
23201db5ce
commit
32e65a7742
|
|
@ -32,6 +32,14 @@
|
||||||
<artifactId>org.eclipse.jgit</artifactId>
|
<artifactId>org.eclipse.jgit</artifactId>
|
||||||
<version>7.3.0.202506031305-r</version>
|
<version>7.3.0.202506031305-r</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.hierynomus/sshj -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.hierynomus</groupId>
|
||||||
|
<artifactId>sshj</artifactId>
|
||||||
|
<version>0.39.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
package ru.kirillius.pf.sdn;
|
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.Networking.NetworkResourceBundle;
|
||||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
|
||||||
import ru.kirillius.utils.logging.SystemLogger;
|
import ru.kirillius.utils.logging.SystemLogger;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public class App extends AppContext {
|
public class App extends AppContext {
|
||||||
private final static File configFile = new File("config.json");
|
private final static File configFile = new File("config.json");
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
public App(File configFile) {
|
public App(File configFile) {
|
||||||
super(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 {
|
static {
|
||||||
|
|
@ -28,7 +30,15 @@ public class App extends AppContext {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
try (App app = new App(configFile)) {
|
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) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(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.ContextEventsHandler;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
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.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AppContext implements Context, Closeable {
|
public class AppContext implements Context, Closeable {
|
||||||
|
|
||||||
|
protected final static String CTX = AppContext.class.getSimpleName();
|
||||||
|
|
||||||
public AppContext(File configFile) {
|
public AppContext(File configFile) {
|
||||||
try {
|
try {
|
||||||
config = Config.load(configFile);
|
config = Config.load(configFile);
|
||||||
|
|
@ -34,6 +41,30 @@ public class AppContext implements Context, Closeable {
|
||||||
ASInfoService.setProvider(new HEInfoProvider(this));
|
ASInfoService.setProvider(new HEInfoProvider(this));
|
||||||
networkManager = new NetworkManager(this);
|
networkManager = new NetworkManager(this);
|
||||||
networkManager.getInputResources().add(config.getCustomResources());
|
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;
|
private final Server server;
|
||||||
@Getter
|
@Getter
|
||||||
private final ASInfoService ASInfoService;
|
private final ASInfoService ASInfoService;
|
||||||
|
@Getter
|
||||||
|
private final SubscriptionManager subscriptionManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
|
loadedPlugins.forEach(plugin -> {
|
||||||
|
try {
|
||||||
|
plugin.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
SystemLogger.error("Error closing plugin", CTX, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
ASInfoService.close();
|
ASInfoService.close();
|
||||||
networkManager.close();
|
networkManager.close();
|
||||||
try {
|
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.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
|
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.Context;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
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 ru.kirillius.utils.logging.SystemLogger;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.StringJoiner;
|
|
||||||
|
|
||||||
public class GitSubscription implements SubscriptionProvider {
|
public class GitSubscription implements SubscriptionProvider {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
@ -39,10 +42,27 @@ public class GitSubscription implements SubscriptionProvider {
|
||||||
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
|
SystemLogger.message("Fetching git repository " + config.getName() + " (" + config.getSource() + ")", CTX);
|
||||||
checkAndPullUpdates(repository);
|
checkAndPullUpdates(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
repository.close();
|
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) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(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 lombok.Setter;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.json.JSONTokener;
|
import org.json.JSONTokener;
|
||||||
import ru.kirillius.json.JSONArrayProperty;
|
import ru.kirillius.json.*;
|
||||||
import ru.kirillius.json.JSONProperty;
|
|
||||||
import ru.kirillius.json.JSONSerializable;
|
|
||||||
import ru.kirillius.json.JSONUtility;
|
|
||||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
@ -43,6 +41,22 @@ public class Config {
|
||||||
@JSONArrayProperty(type = RepositoryConfig.class)
|
@JSONArrayProperty(type = RepositoryConfig.class)
|
||||||
private List<RepositoryConfig> subscriptions = Collections.emptyList();
|
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
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
|
|
@ -66,7 +80,7 @@ public class Config {
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int mergeSubnetsCount = 10;
|
private int mergeSubnetsWithUsage = 51;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@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.Auth.AuthManager;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||||
|
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||||
|
|
||||||
public interface Context {
|
public interface Context {
|
||||||
Config getConfig();
|
Config getConfig();
|
||||||
|
|
@ -16,4 +17,5 @@ public interface Context {
|
||||||
|
|
||||||
NetworkManager getNetworkManager();
|
NetworkManager getNetworkManager();
|
||||||
ContextEventsHandler getEventsHandler();
|
ContextEventsHandler getEventsHandler();
|
||||||
|
SubscriptionManager getSubscriptionManager();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ru.kirillius.pf.sdn.core.Networking;
|
package ru.kirillius.pf.sdn.core.Networking;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import ru.kirillius.json.JSONSerializable;
|
||||||
import ru.kirillius.json.JSONSerializer;
|
import ru.kirillius.json.JSONSerializer;
|
||||||
import ru.kirillius.json.SerializationException;
|
import ru.kirillius.json.SerializationException;
|
||||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
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.Objects;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@JSONSerializable(IPv4Subnet.Serializer.class)
|
||||||
public class IPv4Subnet {
|
public class IPv4Subnet {
|
||||||
|
|
||||||
public final static class Serializer implements JSONSerializer<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
|
@Getter
|
||||||
private final int prefixLength;
|
private final int prefixLength;
|
||||||
|
|
||||||
|
|
@ -33,12 +35,12 @@ public class IPv4Subnet {
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
IPv4Subnet that = (IPv4Subnet) o;
|
IPv4Subnet that = (IPv4Subnet) o;
|
||||||
return address == that.address && prefixLength == that.prefixLength;
|
return longAddress == that.longAddress && prefixLength == that.prefixLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(address, prefixLength);
|
return Objects.hash(longAddress, prefixLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPv4Subnet(String subnet) {
|
public IPv4Subnet(String subnet) {
|
||||||
|
|
@ -49,20 +51,28 @@ public class IPv4Subnet {
|
||||||
var prefix = Integer.parseInt(split[1]);
|
var prefix = Integer.parseInt(split[1]);
|
||||||
IPv4Util.validatePrefix(prefix);
|
IPv4Util.validatePrefix(prefix);
|
||||||
|
|
||||||
address = IPv4Util.ipAddressToLong(split[0]);
|
longAddress = IPv4Util.ipAddressToLong(split[0]);
|
||||||
prefixLength = prefix;
|
prefixLength = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IPv4Subnet(long longAddress, int prefixLength) {
|
||||||
|
this.longAddress = longAddress;
|
||||||
|
this.prefixLength = prefixLength;
|
||||||
|
}
|
||||||
|
|
||||||
public IPv4Subnet(String address, int prefixLength) {
|
public IPv4Subnet(String address, int prefixLength) {
|
||||||
IPv4Util.validatePrefix(prefixLength);
|
IPv4Util.validatePrefix(prefixLength);
|
||||||
|
|
||||||
this.address = IPv4Util.ipAddressToLong(address);
|
this.longAddress = IPv4Util.ipAddressToLong(address);
|
||||||
this.prefixLength = prefixLength;
|
this.prefixLength = prefixLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long count() {
|
||||||
|
return IPv4Util.calculateCountForPrefixLength(prefixLength);
|
||||||
|
}
|
||||||
|
|
||||||
public String getAddress() {
|
public String getAddress() {
|
||||||
return IPv4Util.longToIpAddress(address);
|
return IPv4Util.longToIpAddress(longAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -77,7 +87,7 @@ public class IPv4Subnet {
|
||||||
return false; //can't overlap larger prefix
|
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 lombok.Getter;
|
||||||
import ru.kirillius.pf.sdn.core.Context;
|
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.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -12,8 +14,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public class NetworkManager implements Closeable {
|
public class NetworkManager implements Closeable {
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private final static String CTX = NetworkManager.class.getSimpleName();
|
||||||
private Context context;
|
private final Context context;
|
||||||
|
|
||||||
public NetworkManager(Context context) {
|
public NetworkManager(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
@ -33,28 +35,43 @@ public class NetworkManager implements Closeable {
|
||||||
|
|
||||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public synchronized void triggerUpdate() {
|
public void triggerUpdate() {
|
||||||
if (isUpdatingNow()) {
|
if (isUpdatingNow()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
SystemLogger.message("Updating network manager", CTX);
|
||||||
|
|
||||||
updateProcess.set(executor.submit(() -> {
|
updateProcess.set(executor.submit(() -> {
|
||||||
|
SystemLogger.message("Update is started", CTX);
|
||||||
var config = context.getConfig();
|
var config = context.getConfig();
|
||||||
var filteredResources = config.getFilteredResources();
|
var filteredResources = config.getFilteredResources();
|
||||||
var asn = new ArrayList<>(inputResources.getASN());
|
var asn = new ArrayList<>(inputResources.getASN());
|
||||||
asn.removeAll(filteredResources.getASN());
|
asn.removeAll(filteredResources.getASN());
|
||||||
|
|
||||||
|
|
||||||
if (cacheInvalid.get()) {
|
if (cacheInvalid.get()) {
|
||||||
fetchPrefixes(asn);
|
fetchPrefixes(asn);
|
||||||
}
|
}
|
||||||
|
|
||||||
var subnets = new HashSet<>(inputResources.getSubnets());
|
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);
|
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());
|
var domains = new HashSet<>(inputResources.getDomains());
|
||||||
filteredResources.getDomains().forEach(domains::remove);
|
filteredResources.getDomains().forEach(domains::remove);
|
||||||
//check overlaps
|
//check domain overlaps
|
||||||
|
|
||||||
var domainsToRemove = new HashSet<String>();
|
var domainsToRemove = new HashSet<String>();
|
||||||
for (var domainToMatch : domains) {
|
for (var domainToMatch : domains) {
|
||||||
|
|
@ -69,9 +86,11 @@ public class NetworkManager implements Closeable {
|
||||||
domains.removeAll(domainsToRemove);
|
domains.removeAll(domainsToRemove);
|
||||||
|
|
||||||
outputResources.setASN(Collections.unmodifiableList(asn));
|
outputResources.setASN(Collections.unmodifiableList(asn));
|
||||||
outputResources.setSubnets(subnets.stream().toList());
|
outputResources.setSubnets(merged.getResult());
|
||||||
outputResources.setDomains(domains.stream().toList());
|
outputResources.setDomains(domains.stream().toList());
|
||||||
|
|
||||||
|
SystemLogger.message("Update is complete", CTX);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources);
|
context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -80,12 +99,14 @@ public class NetworkManager implements Closeable {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void invalidateCache() {
|
public void invalidateCache() {
|
||||||
cacheInvalid.set(true);
|
cacheInvalid.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchPrefixes(List<Integer> systems) {
|
private void fetchPrefixes(List<Integer> systems) {
|
||||||
systems.forEach(as -> {
|
systems.forEach(as -> {
|
||||||
|
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
|
||||||
var service = context.getASInfoService();
|
var service = context.getASInfoService();
|
||||||
var future = service.getPrefixes(as);
|
var future = service.getPrefixes(as);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn.core.Networking;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import ru.kirillius.json.JSONArrayProperty;
|
import ru.kirillius.json.JSONArrayProperty;
|
||||||
|
import ru.kirillius.json.JSONProperty;
|
||||||
import ru.kirillius.json.JSONSerializable;
|
import ru.kirillius.json.JSONSerializable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -12,6 +13,10 @@ import java.util.List;
|
||||||
@Builder
|
@Builder
|
||||||
@JSONSerializable
|
@JSONSerializable
|
||||||
public class NetworkResourceBundle {
|
public class NetworkResourceBundle {
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@JSONProperty(required = false)
|
||||||
|
private String description = "";
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONArrayProperty(type = Integer.class)
|
@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.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
@ -14,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
public class SubscriptionManager implements Closeable {
|
public class SubscriptionManager implements Closeable {
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
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) {
|
public SubscriptionManager(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
@ -38,13 +42,35 @@ public class SubscriptionManager implements Closeable {
|
||||||
updateProcess.set(executor.submit(() -> {
|
updateProcess.set(executor.submit(() -> {
|
||||||
var bundle = new NetworkResourceBundle();
|
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.clear();
|
||||||
outputResources.add(bundle);
|
outputResources.add(bundle);
|
||||||
try {
|
try {
|
||||||
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
|
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
|
||||||
} catch (Exception e) {
|
} 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;
|
package ru.kirillius.pf.sdn.core.Util;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
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.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
public class IPv4Util {
|
public class IPv4Util {
|
||||||
|
|
@ -50,4 +56,219 @@ public class IPv4Util {
|
||||||
}
|
}
|
||||||
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
|
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