Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

57 changed files with 228 additions and 1932 deletions

1
.gitignore vendored
View File

@ -31,4 +31,3 @@ hs_err_pid*
# Idea Editor
/.idea
out/

View File

@ -12,10 +12,6 @@
本程序为脚本提供 SSH 客户端(使用 [apache/Mina-sshd](https://github.com/apache/mina-sshd) ),协助脚本通过 SSH 管理服务器。
### 关于更新慢的说明 ###
这个项目一直在更新(只有仓库存档了才会说不更),有什么不足的,
或者想添加什么功能的可以发 issue最近本人比较忙可能没那么及时但本项目肯定是不会弃坑的 :D
## Docs ##
[中文(简体)](./docs/cn/Index.md)

View File

@ -1,14 +1,12 @@
//file:noinspection GradlePackageUpdate SpringBoot .
//file:noinspection GroovyAssignabilityCheck
plugins {
id 'java'
id 'org.springframework.boot' version '2.5.4'
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'jacoco'
}
group 'net.lamgc'
version '0.2.0'
group 'net.lamgc.oracle'
version '0.1.0'
compileJava.sourceCompatibility = JavaVersion.VERSION_16
repositories {
@ -16,18 +14,17 @@ repositories {
}
dependencies {
def ociSdkVer = '2.4.0'
def ociSdkVer = '2.3.2'
def sshdVer = '2.7.0'
def bouncyCastleVer = '1.69'
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.slf4j:slf4j-api:1.7.32'
implementation 'org.slf4j:slf4j-api:1.7.31'
implementation "com.oracle.oci.sdk:oci-java-sdk-core:${ociSdkVer}"
implementation "com.oracle.oci.sdk:oci-java-sdk-identity:${ociSdkVer}"
implementation "com.oracle.oci.sdk:oci-java-sdk-objectstorage:${ociSdkVer}"
implementation "org.apache.sshd:sshd-core:${sshdVer}"
implementation "org.apache.sshd:sshd-sftp:${sshdVer}"
@ -35,16 +32,14 @@ dependencies {
implementation "org.bouncycastle:bcpg-jdk15on:${bouncyCastleVer}"
implementation "org.bouncycastle:bcpkix-jdk15on:${bouncyCastleVer}"
implementation 'cglib:cglib:3.3.0'
implementation "net.i2p.crypto:eddsa:0.3.0"
implementation 'org.codehaus.groovy:groovy-all:3.0.8'
implementation 'org.codehaus.groovy:groovy-all:3.0.7'
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'com.google.code.gson:gson:2.8.7'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}
test {

View File

@ -1,12 +1,11 @@
package net.lamgc.oracle.sentry;
import com.google.common.base.Throwables;
import net.lamgc.oracle.sentry.oci.account.OracleAccount;
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.ScriptComponentExtension;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import net.lamgc.oracle.sentry.script.ScriptComponents;
import net.lamgc.oracle.sentry.script.ScriptManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -22,7 +21,6 @@ import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ServiceLoader;
/**
* @author LamGC
@ -49,8 +47,8 @@ class ApplicationInitiation {
private String sshIdentityPath;
@Bean("oracle.identity.manager")
public OracleAccountManager initialOracleAccountManager() throws IOException {
OracleAccountManager oracleUserManager = new OracleAccountManager();
public OracleIdentityManager initialOracleIdentityManager() throws IOException {
OracleIdentityManager oracleUserManager = new OracleIdentityManager();
log.info("正在加载 Oracle API 身份配置...");
log.debug("Oracle API 身份配置查找路径: \"{}\", 匹配表达式: {}", identityDirectory, identityFilePattern);
File identityDir = new File(identityDirectory);
@ -69,15 +67,15 @@ class ApplicationInitiation {
@Bean("oracle.compute.instance.manager")
@Autowired
public ComputeInstanceManager initialComputeInstanceManager(OracleAccountManager accountManager) throws IOException {
public ComputeInstanceManager initialComputeInstanceManager(OracleIdentityManager identityManager) throws IOException {
ComputeInstanceManager instanceManager = new ComputeInstanceManager();
int addTotal = 0;
for (OracleAccount account : accountManager.getAccounts()) {
String identityName = account.name();
for (AuthenticationDetailsProvider provider : identityManager.getProviders()) {
String identityName = identityManager.getIdentityName(provider.getUserId());
log.info("正在加载用户 {} 所拥有的所有实例...", identityName);
int addCount;
try {
addCount = instanceManager.addComputeInstanceFromUser(account);
addCount = instanceManager.addComputeInstanceFromUser(provider);
} catch (Exception e) {
log.error("加载实例时发生异常.", e);
continue;
@ -94,33 +92,18 @@ class ApplicationInitiation {
@Bean("sentry.script.manager")
@Autowired
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager, OracleAccountManager accountManager) {
ScriptComponents components = new ScriptComponents();
components.addComponentObject("InstanceManager", instanceManager);
components.addComponentObject("AccountManager", accountManager);
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager) {
ScriptComponents context = new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create()
.build()),
instanceManager);
configureScriptComponentsFromExtension(components);
ScriptManager manager = new ScriptManager(new File(scriptsLocation), components);
ScriptManager manager = new ScriptManager(new File(scriptsLocation), context);
manager.loadScripts();
return manager;
}
private void configureScriptComponentsFromExtension(ScriptComponents components) {
ServiceLoader<ScriptComponentExtension> extensions = ServiceLoader.load(ScriptComponentExtension.class);
for (ScriptComponentExtension extension : extensions) {
try {
extension.configureScriptComponents(components);
} catch (Exception e) {
log.error("脚本组件扩展配置组件时发生未捕获异常.(Extension: {})\n{}",
extension.getClass().getName(), Throwables.getStackTraceAsString(e));
}
}
}
@PostConstruct
@Order(1)
@SuppressWarnings({"MismatchedReadAndWriteOfArray", "RedundantOperationOnEmptyContainer"})
private void initialEnvironment() throws IOException {
String[] directors = new String[] {
"./config",
@ -129,7 +112,7 @@ class ApplicationInitiation {
};
String[] files = new String[] {
sshIdentityPath
};
for (String directory : directors) {

View File

@ -12,7 +12,7 @@ import org.springframework.context.annotation.Configuration;
*/
@SpringBootApplication
@Configuration
class ApplicationMain {
public class ApplicationMain {
private final static Logger log = LoggerFactory.getLogger(ApplicationMain.class);

View File

@ -1,22 +1,20 @@
package net.lamgc.oracle.sentry.oci.compute;
package net.lamgc.oracle.sentry;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.core.ComputeClient;
import com.oracle.bmc.core.model.Instance;
import com.oracle.bmc.core.requests.ListInstancesRequest;
import com.oracle.bmc.core.responses.ListInstancesResponse;
import com.oracle.bmc.identity.IdentityClient;
import com.oracle.bmc.identity.model.Compartment;
import com.oracle.bmc.identity.requests.ListCompartmentsRequest;
import com.oracle.bmc.identity.responses.ListCompartmentsResponse;
import net.lamgc.oracle.sentry.oci.account.OracleAccount;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
import net.lamgc.oracle.sentry.oci.compute.ssh.SshAuthIdentityProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@ -24,9 +22,7 @@ import java.util.stream.Collectors;
* 计算实例管理器.
* @author LamGC
*/
public final class ComputeInstanceManager {
private final static Logger log = LoggerFactory.getLogger(ComputeInstanceManager.class);
public class ComputeInstanceManager {
private final Map<String, ComputeInstance> instanceMap = new ConcurrentHashMap<>();
private SshAuthIdentityProvider sshIdentityProvider;
@ -37,10 +33,8 @@ public final class ComputeInstanceManager {
* @throws IOException 加载时如有异常将直接抛出.
*/
public void initialSshIdentityProvider(File sshIdentityJson) throws IOException {
log.debug("正在初始化 SSH 认证配置提供器...");
sshIdentityProvider = new SshAuthIdentityProvider(this, sshIdentityJson);
sshIdentityProvider.loadAuthInfo();
log.debug("SSH 认证配置提供器已初始化完成.");
}
/**
@ -70,41 +64,39 @@ public final class ComputeInstanceManager {
public Set<ComputeInstance> getInstancesByUserId(String userId) {
Objects.requireNonNull(userId);
return instanceMap.values().stream()
.filter(computeInstance -> computeInstance.getFromAccount().id().equals(userId))
.filter(computeInstance -> computeInstance.getUserId().equals(userId))
.collect(Collectors.toSet());
}
/**
* 添加某一用户的所有计算实例.
* @param account Oracle 云账号对象.
* @param provider 用户身份提供器.
* @return 返回已成功添加的实例数量.
* @throws NullPointerException 如果 provider {@code null} 则抛出异常.
*/
public int addComputeInstanceFromUser(OracleAccount account) {
Objects.requireNonNull(account);
ListCompartmentsResponse listCompartments = account.clients().identity()
.listCompartments(ListCompartmentsRequest.builder()
.compartmentId(account.tenantId())
.build());
public int addComputeInstanceFromUser(AuthenticationDetailsProvider provider) {
Objects.requireNonNull(provider);
IdentityClient identityClient = new IdentityClient(provider);
ComputeClient computeClient = new ComputeClient(provider);
ListCompartmentsResponse listCompartments = identityClient.listCompartments(ListCompartmentsRequest.builder()
.compartmentId(provider.getTenantId())
.build());
int addCount = 0;
Set<String> compartmentIds = listCompartments.getItems().stream()
.map(Compartment::getId).collect(Collectors.toSet());
compartmentIds.add(account.tenantId());
compartmentIds.add(provider.getTenantId());
for (String compartmentId : compartmentIds) {
ListInstancesResponse listInstances = account.clients().compute()
.listInstances(ListInstancesRequest.builder()
.compartmentId(compartmentId)
.build());
ListInstancesResponse listInstances = computeClient.listInstances(ListInstancesRequest.builder()
.compartmentId(compartmentId)
.build());
for (Instance instance : listInstances.getItems()) {
if (instance.getLifecycleState() == Instance.LifecycleState.Terminated ||
instance.getLifecycleState() == Instance.LifecycleState.Terminating) {
log.debug("实例 {} 状态为 {}, 不添加该实例.", instance.getId(), instance.getLifecycleState());
if (instance.getLifecycleState() == Instance.LifecycleState.Terminated) {
continue;
}
ComputeInstance computeInstance = new ComputeInstance(this, instance.getId(),
compartmentId, instance.getImageId(), account);
provider.getUserId(), compartmentId, instance.getImageId(), provider);
addComputeInstance(computeInstance);
log.debug("已为用户 {} 添加计算实例: {}", account.id(), instance.getId());
addCount ++;
}
}

View File

@ -1,7 +1,5 @@
package net.lamgc.oracle.sentry;
import net.lamgc.oracle.sentry.oci.compute.ssh.ConfiguredForwardingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@ -25,8 +23,6 @@ public final class Constants {
@NonNull
private String firstConnectionPolicy;
@Autowired
private ConfiguredForwardingFilter forwardingFilter;
/**
* 获取 SSH 首次连接策略.
@ -36,12 +32,4 @@ public final class Constants {
public String getFirstConnectionPolicy() {
return firstConnectionPolicy;
}
/**
* 获取已配置的转发过滤器.
* @return 返回转发过滤器.
*/
public ConfiguredForwardingFilter getForwardingFilter() {
return forwardingFilter;
}
}

View File

@ -1,15 +1,17 @@
package net.lamgc.oracle.sentry.oci.account;
package net.lamgc.oracle.sentry;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.oracle.bmc.ConfigFileReader;
import com.oracle.bmc.Region;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimplePrivateKeySupplier;
import net.lamgc.oracle.sentry.common.OracleBmcExceptionHandler;
import net.lamgc.oracle.sentry.common.retry.ExponentialBackoffDelayer;
import net.lamgc.oracle.sentry.common.retry.Retryer;
import com.oracle.bmc.identity.IdentityClient;
import com.oracle.bmc.identity.requests.GetUserRequest;
import com.oracle.bmc.identity.responses.GetUserResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -21,25 +23,30 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Oracle 云账号管理器.
* Oracle 身份管理器.
* @author LamGC
*/
public final class OracleAccountManager {
public final class OracleIdentityManager {
private final static Logger log = LoggerFactory.getLogger(OracleAccountManager.class);
private final static Logger log = LoggerFactory.getLogger(OracleIdentityManager.class);
/**
* 认证身份 Map.
* Key: Identity Id
* Value {@link AuthenticationDetailsProvider}
*/
private final Map<String, OracleAccount> accountMap = new ConcurrentHashMap<>();
private final Map<String, AuthenticationDetailsProvider> identityMap = new ConcurrentHashMap<>();
/**
* 用户名 Map.
* key Identity Id
* Value: Username
*/
private final Map<String, String> identityNameMap = new ConcurrentHashMap<>();
/**
* 从目录扫描匹配的配置文件并加载.
@ -59,39 +66,23 @@ public final class OracleAccountManager {
if (configFiles == null) {
throw new IOException("Unable to access the specified directory: " + directory.getCanonicalPath());
}
AtomicInteger loadedCount = new AtomicInteger();
int loadedCount = 0;
for (File configFile : configFiles) {
try {
Retryer<OracleAccount> retryer = Retryer.builder(() -> {
OracleAccount account = loadFromConfigFile(configFile);
if (account == null) {
return null;
}
log.info("已成功加载身份配置文件.\n\tUserId: {}\n\tUsername: {}\n\tPath: {}",
account.id(),
account.name(),
configFile.getCanonicalPath());
return account;
})
.delayer(new ExponentialBackoffDelayer())
.exceptionHandler(new OracleBmcExceptionHandler())
.retryNumber(8)
.create();
Future<OracleAccount> future = retryer.executeAsync();
future.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
if (e instanceof TimeoutException) {
AuthenticationDetailsProvider provider = loadFromConfigFile(configFile);
if (provider == null) {
continue;
} else if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
break;
}
log.error("加载身份配置文件时发生异常.(Path: {})\n{}", configFile.getCanonicalPath(), Throwables.getStackTraceAsString(e.getCause()));
loadedCount ++;
log.info("已成功加载身份配置文件.\n\tUserId: {}\n\tUsername: {}\n\tPath: {}",
provider.getUserId(),
getIdentityName(provider.getUserId()),
configFile.getCanonicalPath());
} catch (Exception e) {
log.error("加载身份配置文件时发生异常.(Path: {})\n{}", configFile.getCanonicalPath(), Throwables.getStackTraceAsString(e));
}
}
return loadedCount.get();
return loadedCount;
}
/**
@ -101,7 +92,7 @@ public final class OracleAccountManager {
* @return 返回已成功加载后, 配置文件对应的身份配置提供器.
* @throws IOException 如果读取文件发生问题时将抛出该异常.
*/
public OracleAccount loadFromConfigFile(File identityConfig) throws IOException {
public AuthenticationDetailsProvider loadFromConfigFile(File identityConfig) throws IOException {
if (!identityConfig.exists()) {
throw new FileNotFoundException(identityConfig.getAbsolutePath());
}
@ -131,14 +122,10 @@ public final class OracleAccountManager {
.build();
// 尝试获取身份所属用户名, 以此检查该身份配置是否正确.
OracleAccount oracleAccount = new OracleAccount(provider);
String accountName = oracleAccount.name();
if (accountName == null) {
throw new NullPointerException("Failed to obtain the account name. The identity configuration may be incorrect.");
}
log.debug("已成功通过身份配置获取用户名称: {}", accountName);
accountMap.put(oracleAccount.id(), oracleAccount);
return oracleAccount;
String identityName = getIdentityName0(provider);
identityNameMap.put(provider.getUserId(), identityName);
identityMap.put(provider.getUserId(), provider);
return provider;
}
private boolean checkIdentityProfileConfig(ConfigFileReader.ConfigFile config) {
@ -157,6 +144,35 @@ public final class OracleAccountManager {
return true;
}
/**
* 获取身份所属用户的名称.
* @param provider 身份提供器.
* @return 返回用户名.
*/
private String getIdentityName0(AuthenticationDetailsProvider provider) {
IdentityClient identityClient = new IdentityClient(provider);
GetUserResponse user = identityClient.getUser(GetUserRequest.builder()
.userId(provider.getUserId())
.build());
return user.getUser().getName();
}
/**
* 获取身份信息所属的用户名.
* @param userId 身份信息所属的用户 Id.
* @return 返回用户名.
* @throws NullPointerException userId {@code null} 时抛出该异常.
* @throws NoSuchElementException 指定的 UserId 未找到对应用户名时抛出该异常.
*/
public String getIdentityName(String userId) {
Objects.requireNonNull(userId);
if (!identityMap.containsKey(userId)) {
throw new NoSuchElementException(userId);
}
return identityNameMap.get(userId);
}
/**
* 通过 UserId 获取指定身份提供器.
* @param userId 用户 Id.
@ -164,20 +180,37 @@ public final class OracleAccountManager {
* @throws NullPointerException userId {@code null} 时抛出该异常.
* @throws NoSuchElementException 指定的 UserId 未找到对应 Provider 时抛出该异常.
*/
public OracleAccount getAccountByUserId(String userId) {
public AuthenticationDetailsProvider getProviderByUserId(String userId) {
Objects.requireNonNull(userId);
if (!accountMap.containsKey(userId)) {
if (!identityMap.containsKey(userId)) {
throw new NoSuchElementException(userId);
}
return accountMap.get(userId);
return identityMap.get(userId);
}
/**
* 导出身份信息.
* <p> 不包含私钥.
* @return 返回 Json 形式的身份信息数组.
*/
public JsonArray exportIdentityInfo() {
JsonArray identityInfoArray = new JsonArray(identityMap.size());
for (AuthenticationDetailsProvider provider : identityMap.values()) {
JsonObject identity = new JsonObject();
identity.addProperty("UserId", provider.getUserId());
identity.addProperty("TenantId", provider.getTenantId());
identity.addProperty("Fingerprint", provider.getFingerprint());
identityInfoArray.add(identity);
}
return identityInfoArray;
}
/**
* 获取所有身份提供器.
* @return 返回包含所有身份提供器的集合对象.
*/
public Set<OracleAccount> getAccounts() {
return accountMap.values().stream().collect(Collectors.toUnmodifiableSet());
public Set<AuthenticationDetailsProvider> getProviders() {
return identityMap.values().stream().collect(Collectors.toUnmodifiableSet());
}
}

View File

@ -1,4 +1,4 @@
package net.lamgc.oracle.sentry.common.io;
package net.lamgc.oracle.sentry.common;
import java.io.IOException;
import java.io.InputStream;

View File

@ -1,48 +0,0 @@
package net.lamgc.oracle.sentry.common;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
/**
* 惰性加载器.
* <p> 该加载器只会在第一次获取对象时初始化.
* <p> 如果不用, 可能会导致内存消耗增加, 尤其是在管理大量帐号的情况下.
* <p> 对象在本加载器中为单例, 任何通过同一个加载器获取的对象都是同一个对象.
* @author LamGC
* @param <T> 对象类型.
*/
public final class LazyLoader<T> {
private final Supplier<T> supplier;
private final AtomicReference<T> object = new AtomicReference<>(null);
/**
* 构建惰性加载器.
* @param supplier 对象提供器.
*/
public LazyLoader(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
/**
* 获取对象.
* <p> 如果是首次调用本方法, 本方法将通过 Supplier 获取一个对象, 缓存对象后返回.
* @return 返回惰性加载的对象.
*/
public T getInstance() {
if (object.get() == null) {
synchronized (this) {
if (object.get() == null) {
T newInstance = supplier.get();
if (newInstance == null) {
throw new NullPointerException("Supplier is not allowed to return null.");
}
return object.compareAndSet(null, newInstance) ? newInstance : object.get();
}
}
}
return object.get();
}
}

View File

@ -1,14 +0,0 @@
package net.lamgc.oracle.sentry.common;
import com.oracle.bmc.model.BmcException;
import net.lamgc.oracle.sentry.common.retry.RetryExceptionHandler;
public class OracleBmcExceptionHandler implements RetryExceptionHandler {
@Override
public boolean handle(Exception e) {
if (e instanceof BmcException bmc) {
return bmc.getStatusCode() == -1;
}
return true;
}
}

View File

@ -1,4 +1,4 @@
package net.lamgc.oracle.sentry.common.io;
package net.lamgc.oracle.sentry.common;
import java.io.IOException;
import java.io.OutputStream;

View File

@ -1,53 +0,0 @@
package net.lamgc.oracle.sentry.common.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
/**
* 级别范围过滤器.
* @author LamGC
*/
@SuppressWarnings("unused")
public class LevelRangeFilter extends Filter<ILoggingEvent> {
private Level maxLevel;
private Level minLevel;
@Override
public FilterReply decide(ILoggingEvent event) {
int level = event.getLevel().levelInt;
if (level > maxLevel.levelInt || level < minLevel.levelInt) {
return FilterReply.DENY;
}
return FilterReply.NEUTRAL;
}
/**
* 设置最高等级.
* <p> 仅限 XML 配置文件设置.
* @param maxLevel 最高等级(包括).
*/
public void setMaxLevel(String maxLevel) {
this.maxLevel = Level.toLevel(maxLevel);
}
/**
* 设置最低等级.
* @param minLevel 允许的最低等级(包括).
*/
public void setMinLevel(String minLevel) {
this.minLevel = Level.toLevel(minLevel);
}
@Override
public void start() {
if (maxLevel != null && minLevel != null) {
if (maxLevel.levelInt < minLevel.levelInt) {
throw new IllegalArgumentException("The maximum level cannot be less than the minimum level.");
}
super.start();
}
}
}

View File

@ -1,55 +0,0 @@
package net.lamgc.oracle.sentry.common.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.Marker;
/**
* @author LamGC
*/
@SuppressWarnings("unused")
public class MarkerFilter extends Filter<ILoggingEvent> {
private String markerName;
private FilterReply onMatch = FilterReply.NEUTRAL;
private FilterReply onMismatch = FilterReply.DENY;
@Override
public FilterReply decide(ILoggingEvent event) {
return event.getMarker() != null && event.getMarker().getName().equals(markerName) ? onMatch : onMismatch;
}
/**
* 设置要匹配的 {@link Marker} 名称.
* @param markerName Marker 名称.
*/
public void setMarkerName(String markerName) {
this.markerName = markerName;
}
/**
* 如果匹配, 需要执行的操作.
* @see FilterReply
* @param onMatch 操作名称.
*/
public void setOnMatch(String onMatch) {
this.onMatch = FilterReply.valueOf(onMatch);
}
/**
* 如果不匹配, 需要执行的操作.
* @see FilterReply
* @param onMismatch 操作名称.
*/
public void setOnMismatch(String onMismatch) {
this.onMismatch = FilterReply.valueOf(onMismatch);
}
@Override
public void start() {
if (markerName != null && onMatch != null && onMismatch != null) {
super.start();
}
}
}

View File

@ -1,15 +0,0 @@
package net.lamgc.oracle.sentry.common.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
/**
* @author LamGC
*/
public class NoMarkerFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
return event.getMarker() != null ? FilterReply.DENY : FilterReply.NEUTRAL;
}
}

View File

@ -1,14 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 断言校验器.
* <p> 校验器可帮助 {@link Retryer} 检查执行结果是否符合预期, 如果不符合预期, 那么将会重新执行任务.
* @param <R> 执行结果类型.
* @author LamGC
*/
@FunctionalInterface
public interface AssertionChecker<R> {
void check(R result) throws RetryAssertException;
}

View File

@ -1,14 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 指数退避延迟器.
* @author LamGC
*/
public final class ExponentialBackoffDelayer implements RetryDelayer {
@Override
public long nextDelayTime(int currentRetryCount) {
return (4L << currentRetryCount) * 1000;
}
}

View File

@ -1,26 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
import java.util.concurrent.TimeUnit;
/**
* 固定延迟器.
* <p> 永远以指定的延迟重试.
* @author LamGC
*/
public final class FixedTimeDelayer implements RetryDelayer {
private final long delay;
public FixedTimeDelayer(long time, TimeUnit unit) {
this(unit.toMillis(time));
}
public FixedTimeDelayer(long delay) {
this.delay = delay;
}
@Override
public long nextDelayTime(int currentRetryCount) {
return delay;
}
}

View File

@ -1,27 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 非空检查器.
* <p> 当执行结果为 null 时断言失败.
* @param <R>
* @author LamGC
*/
public final class NonNullChecker<R> implements AssertionChecker<R> {
@SuppressWarnings("rawtypes")
private final static NonNullChecker INSTANCE = new NonNullChecker();
@SuppressWarnings("unchecked")
public static <R> NonNullChecker<R> getInstance() {
return (NonNullChecker<R>) INSTANCE;
}
private NonNullChecker() {}
@Override
public void check(Object result) throws RetryAssertException {
if (result == null) {
throw new RetryAssertException("The execution result is null.");
}
}
}

View File

@ -1,18 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 重试断言异常.
* <p> 当指定条件失败时抛出该异常.
* <p> 跑出该异常的原因并非任务执行失败, 而是任务执行的结果与预期不符.
* @author LamGC
*/
public class RetryAssertException extends Exception {
public RetryAssertException(String message) {
super(message);
}
public RetryAssertException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,17 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 重试延迟器.
* <p> 用于决定每次重试的间隔时间, 可根据重试次数调整间隔时常, 以避免频繁执行影响性能.
* @author LamGC
*/
public interface RetryDelayer {
/**
* 获取下一次重试延迟时间.
* @param currentRetryCount 当前重试次数, 如果第一次重试失败, 则本参数为 0.
* @return 返回延迟时间.
*/
long nextDelayTime(int currentRetryCount);
}

View File

@ -1,13 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
@FunctionalInterface
public interface RetryExceptionHandler {
/**
* 处理异常, 并指示是否继续重试.
* @param e 异常对象.
* @return 如果可以继续重试, 返回 {@code true}.
*/
boolean handle(Exception e);
}

View File

@ -1,20 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 重试失败异常.
* <p> 该异常是由于某一个任务尝试执行失败过多而抛出, 失败的原因可能是任务执行时抛出异常, 或执行结果与预期不符.
*/
public final class RetryFailedException extends Exception {
public RetryFailedException(String message) {
super(message);
}
public RetryFailedException(String message, Throwable cause) {
super(message, cause);
}
public RetryFailedException(Throwable cause) {
super(cause);
}
}

View File

@ -1,20 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
/**
* 可重试任务.
* <p> 实现该方法后, 该任务可在 {@link Retryer} 运行, 可用于一些需重试的任务.
* @param <R> 执行结果类型.
*/
@FunctionalInterface
public interface RetryableTask<R> {
/**
* 运行方法.
* <p> 当该方法抛出异常, 或经 {@link AssertionChecker} 检查认为结果与预期不符时, 将会被重新运行.
* <p> 请注意, 即使任务执行完成, {@link AssertionChecker} 认为结果与预期不符, 任务将会被重新运行, 请注意处理该情况.
* @throws Exception 当异常抛出时, 将视为执行失败, 重试器将根据设定自动重新执行.
* @return 根据需要可返回.
*/
R run() throws Exception;
}

View File

@ -1,177 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.*;
import java.util.concurrent.*;
/**
* 重试器.
* <p> 通过重试器, 可以对某一个可能失败的任务做重试, 尽可能确保任务执行成功.
* @param <R> 任务结果类型.
* @author LamGC
*/
public final class Retryer<R> {
private final static ThreadPoolExecutor ASYNC_EXECUTOR = new ThreadPoolExecutor(
1,
Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)),
10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
new ThreadFactoryBuilder()
.setNameFormat("Thread-Retryer-%d")
.build()
);
private final RetryableTask<R> task;
private final int retryNumber;
private final RetryDelayer delayer;
private final Set<AssertionChecker<R>> checkers = new HashSet<>();
private final RetryExceptionHandler handler;
private Retryer(RetryableTask<R> task, int retryNumber, RetryDelayer delayer, Set<AssertionChecker<R>> checkers, RetryExceptionHandler handler) {
this.handler = handler;
if (retryNumber < 0) {
throw new IllegalArgumentException("The number of retries is not allowed to be negative: " + retryNumber);
}
this.task = Objects.requireNonNull(task);
this.delayer = Objects.requireNonNull(delayer);
this.retryNumber = retryNumber;
if (checkers != null && !checkers.isEmpty()) {
this.checkers.addAll(checkers);
}
}
@SuppressWarnings("BusyWait")
private R execute0() throws Exception {
Exception lastException;
int currentRetryCount = 0;
do {
try {
R result = task.run();
checkResult(result);
return result;
} catch (Exception e) {
lastException = e;
if (e instanceof InterruptedException) {
break;
}
if (handler != null && !handler.handle(e)) {
break;
}
if (currentRetryCount >= retryNumber) {
break;
}
long delayTime = delayer.nextDelayTime(currentRetryCount);
if (delayTime > 0) {
try {
Thread.sleep(delayTime);
} catch (InterruptedException interrupted) {
break;
}
}
currentRetryCount ++;
}
} while (true);
throw lastException;
}
/**
* 使用 {@link AssertionChecker} 检查结果是否符合预期.
* <p> 当结果不符合检验器预期时, 检验器将抛出 {@link RetryAssertException} 来表示结果不符预期,
* {@link Retryer} 将会重试该任务.
* @param result 执行结果.
* @throws RetryAssertException 当断言检验器对结果断言失败时抛出该异常.
*/
private void checkResult(R result) throws RetryAssertException {
for (AssertionChecker<R> checker : checkers) {
checker.check(result);
}
}
/**
* 异步执行任务.
* <p> 使用线程池执行 task.
* @return 返回 Future 对象以跟踪异步执行结果.
*/
public Future<R> executeAsync() {
return ASYNC_EXECUTOR.submit(this::execute0);
}
/**
* 同步执行任务.
* @return 如果执行完成且成功, 返回执行结果.
* @throws RetryFailedException 当重试多次仍失败时抛出该异常.
*/
public R execute() throws RetryFailedException {
Future<R> future = executeAsync();
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
if (e instanceof ExecutionException exception) {
throw new RetryFailedException("Failure after a certain number of attempts.", exception);
} else {
throw new RetryFailedException(e);
}
}
}
/**
* 获取一个构建器.
* @param task 需要重试的任务.
* @param <R> 结果类型.
* @return 返回新的构造器.
*/
public static <R> Builder<R> builder(RetryableTask<R> task) {
return new Builder<>(task);
}
/**
* {@link Retryer} 构造器.
* <p> 可通过链式调用快速创建 {@link Retryer}.
* @param <R> 任务结果类型.
*/
public static class Builder<R> {
private final RetryableTask<R> task;
private RetryDelayer delayer = new FixedTimeDelayer(0);
private int retryNumber = 0;
private final Set<AssertionChecker<R>> checkers = new HashSet<>();
private RetryExceptionHandler handler = (e) -> true;
private Builder(RetryableTask<R> task) {
this.task = task;
}
public Retryer<R> create() {
return new Retryer<>(task, retryNumber, delayer, checkers, handler);
}
public Builder<R> delayer(RetryDelayer delayer) {
this.delayer = delayer;
return this;
}
public Builder<R> retryIfReturnNull() {
this.checkers.add(NonNullChecker.getInstance());
return this;
}
public Builder<R> retryNumber(int number) {
this.retryNumber = number;
return this;
}
public Builder<R> checker(AssertionChecker<R> checker) {
checkers.add(checker);
return this;
}
public Builder<R> exceptionHandler(RetryExceptionHandler handler) {
this.handler = handler == null ? (e) -> true : handler;
return this;
}
}
}

View File

@ -1,116 +0,0 @@
package net.lamgc.oracle.sentry.oci.account;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.identity.model.RegionSubscription;
import com.oracle.bmc.identity.model.User;
import com.oracle.bmc.identity.requests.GetUserRequest;
import com.oracle.bmc.identity.requests.ListRegionSubscriptionsRequest;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* Oracle 云账户.
* @author LamGC
*/
public final class OracleAccount {
private final AuthenticationDetailsProvider provider;
private final OracleClients clients;
private final User user;
OracleAccount(AuthenticationDetailsProvider provider) {
this.provider = Objects.requireNonNull(provider);
this.clients = new OracleClients(provider);
this.user = clients.identity().getUser(GetUserRequest.builder()
.userId(provider.getUserId())
.build()).getUser();
}
/**
* 获取帐号 Id.
* @return 返回帐号 OCID.
*/
public String id() {
return provider.getUserId();
}
/**
* 获取租户 Id.
* <p> Id 同时也是根区间 Id.
* @return 返回租户 Id.
*/
public String tenantId() {
return provider.getTenantId();
}
/**
* 获取用户名.
* @return 返回用户名称.
*/
public String name() {
return this.user.getName();
}
/**
* 获取用户说明信息.
* @return 返回设定的用户说明信息.
*/
public String description() {
return this.user.getDescription();
}
/**
* 获取用户订阅的所有地区.
* @return 返回已订阅地区列表.
*/
public List<RegionSubscription> regions() {
return this.clients.identity().listRegionSubscriptions(ListRegionSubscriptionsRequest.builder()
.tenancyId(this.tenantId())
.build()).getItems();
}
/**
* 获取帐号主区域.
* @return 返回帐号主区域.
* @throws NoSuchElementException 当主区域搜索失败时抛出.
*/
public RegionSubscription mainRegion() {
for (RegionSubscription subscription : regions()) {
if (subscription.getIsHomeRegion()) {
return subscription;
}
}
throw new NoSuchElementException("Primary region not found.");
}
/**
* 获取该账户所属的 API 客户端集合.
* @return 返回该账户所属的甲骨文 API 客户端集.
*/
public OracleClients clients() {
return clients;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AuthenticationDetailsProvider thatProvider = ((OracleAccount) o).provider;
return provider.getUserId().equals(thatProvider.getUserId()) &&
provider.getTenantId().equals(thatProvider.getTenantId()) &&
provider.getFingerprint().equals(thatProvider.getFingerprint()) &&
provider.getKeyId().equals(thatProvider.getKeyId());
}
@Override
public int hashCode() {
return Objects.hash(provider.getUserId(), provider.getTenantId(), provider.getFingerprint(), provider.getKeyId());
}
}

View File

@ -1,110 +0,0 @@
package net.lamgc.oracle.sentry.oci.account;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.core.BlockstorageClient;
import com.oracle.bmc.core.ComputeClient;
import com.oracle.bmc.core.VirtualNetworkClient;
import com.oracle.bmc.identity.IdentityClient;
import com.oracle.bmc.objectstorage.ObjectStorageClient;
import net.lamgc.oracle.sentry.common.LazyLoader;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* 甲骨文 SDK 客户端.
* @author LamGC
*/
public class OracleClients {
private final Map<Class<?>, LazyLoader<?>> LAZY_LOADER_MAP = new ConcurrentHashMap<>();
private final AuthenticationDetailsProvider provider;
OracleClients(AuthenticationDetailsProvider provider) {
this.provider = Objects.requireNonNull(provider);
initialLazyLoad();
}
private void initialLazyLoad() {
registryLazyLoader(ComputeClient.class, () -> new ComputeClient(provider));
registryLazyLoader(VirtualNetworkClient.class, () -> new VirtualNetworkClient(provider));
registryLazyLoader(BlockstorageClient.class, () -> new BlockstorageClient(provider));
registryLazyLoader(IdentityClient.class, () -> new IdentityClient(provider));
registryLazyLoader(ObjectStorageClient.class, () -> new ObjectStorageClient(provider));
}
/**
* 获取计算类客户端.
* @return 获取计算类客户端对象.
*/
public ComputeClient compute() {
return getInstance(ComputeClient.class);
}
/**
* 获取网络客户端.
* @return 返回 VCN 操作客户端.
*/
public VirtualNetworkClient network() {
return getInstance(VirtualNetworkClient.class);
}
/**
* 获取块存储客户端.
* <p> 仅限计算实例的存储.
* @return 返回块存储客户端.
*/
public BlockstorageClient blockStorage() {
return getInstance(BlockstorageClient.class);
}
/**
* 获取身份客户端.
* <p> 用于访问身份相关 API.
* @return 返回身份客户端.
*/
public IdentityClient identity() {
return getInstance(IdentityClient.class);
}
/**
* 获取对象存储客户端.
* <p> 不包括计算实例的存储.
* @return 获取对象存储客户端.
*/
public ObjectStorageClient objectStorage() {
return getInstance(ObjectStorageClient.class);
}
/**
* 获取实例.
* @param type 实例类.
* @param <T> 实例类型.
* @return 返回对象.
*/
@SuppressWarnings("unchecked")
private <T> T getInstance(Class<?> type) {
Objects.requireNonNull(type);
if (!LAZY_LOADER_MAP.containsKey(type)) {
throw new NoSuchElementException("No lazy loader of this type was found: " + type);
}
return (T) LAZY_LOADER_MAP.get(type).getInstance();
}
/**
* 注册惰性加载器.
* @param type 对象类.
* @param supplier 对象提供器.
* @param <T> 对象类型.
*/
private <T> void registryLazyLoader(Class<? extends T> type, Supplier<T> supplier) {
Objects.requireNonNull(type);
Objects.requireNonNull(supplier);
LAZY_LOADER_MAP.put(type, new LazyLoader<>(supplier));
}
}

View File

@ -1,5 +1,6 @@
package net.lamgc.oracle.sentry.oci.compute;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import com.oracle.bmc.core.ComputeClient;
import com.oracle.bmc.core.model.Instance;
import com.oracle.bmc.core.requests.GetImageRequest;
@ -8,7 +9,7 @@ import com.oracle.bmc.core.requests.InstanceActionRequest;
import com.oracle.bmc.core.responses.GetImageResponse;
import com.oracle.bmc.core.responses.GetInstanceResponse;
import com.oracle.bmc.core.responses.InstanceActionResponse;
import net.lamgc.oracle.sentry.oci.account.OracleAccount;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.oci.compute.ssh.InstanceSsh;
import net.lamgc.oracle.sentry.oci.compute.ssh.SshAuthInfo;
@ -23,10 +24,11 @@ public final class ComputeInstance {
private final ComputeInstanceManager instanceManager;
private final String instanceId;
private final String userId;
private final String compartmentId;
private final String imageId;
private final AuthenticationDetailsProvider authProvider;
private final InstanceNetwork network;
private final OracleAccount fromAccount;
private final ComputeClient computeClient;
@ -34,20 +36,22 @@ public final class ComputeInstance {
* 构造一个计算实例对象.
* @param instanceManager 实例所属的管理器.
* @param instanceId 实例 Id.
* @param userId 所属用户 Id.
* @param compartmentId 实例所在区域的 Id.
* @param imageId 镜像 Id.
* @param fromAccount 所属用户的身份配置提供器.
* @param provider 所属用户的身份配置提供器.
*/
public ComputeInstance(ComputeInstanceManager instanceManager, String instanceId,
String compartmentId, String imageId, OracleAccount fromAccount) {
public ComputeInstance(ComputeInstanceManager instanceManager, String instanceId, String userId,
String compartmentId, String imageId, AuthenticationDetailsProvider provider) {
this.instanceManager = instanceManager;
this.instanceId = instanceId;
this.userId = userId;
this.compartmentId = compartmentId;
this.imageId = imageId;
this.fromAccount = fromAccount;
this.authProvider = provider;
computeClient = new ComputeClient(provider);
this.network = new InstanceNetwork(this);
this.computeClient = fromAccount.clients().compute();
}
/**
@ -59,6 +63,14 @@ public final class ComputeInstance {
return instanceId;
}
/**
* 获取所属用户的 Id.
* @return 返回用户 Id.
*/
public String getUserId() {
return userId;
}
/**
* 获取服务器所属区域的 Id.
* <p> 使用的资源必须要处于同一区域, 例如 IP 资源, 磁盘.
@ -76,8 +88,8 @@ public final class ComputeInstance {
*/
public BootImage getImage() {
GetImageResponse image = computeClient.getImage(GetImageRequest.builder()
.imageId(imageId)
.build());
.imageId(imageId)
.build());
return new BootImage(image.getImage());
}
@ -158,6 +170,10 @@ public final class ComputeInstance {
return computeClient;
}
AuthenticationDetailsProvider getAuthProvider() {
return authProvider;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@ -167,12 +183,12 @@ public final class ComputeInstance {
return false;
}
ComputeInstance that = (ComputeInstance) o;
return instanceId.equals(that.instanceId) && fromAccount.equals(that.fromAccount) && compartmentId.equals(that.compartmentId);
return instanceId.equals(that.instanceId) && userId.equals(that.userId) && compartmentId.equals(that.compartmentId);
}
@Override
public int hashCode() {
return Objects.hash(instanceId, fromAccount, compartmentId);
return Objects.hash(instanceId, userId, compartmentId);
}
/**
@ -185,11 +201,4 @@ public final class ComputeInstance {
.getAuthInfoByInstanceId(instanceId);
}
/**
* 获取实例所属的 Oracle 云帐号对象.
* @return 返回实例所属帐号对象.
*/
public OracleAccount getFromAccount() {
return fromAccount;
}
}

View File

@ -1,7 +1,6 @@
package net.lamgc.oracle.sentry.oci.compute;
import com.oracle.bmc.core.VirtualNetworkClient;
import com.oracle.bmc.core.model.Vnic;
import com.oracle.bmc.core.model.VnicAttachment;
import com.oracle.bmc.core.requests.GetVnicRequest;
import com.oracle.bmc.core.requests.ListVnicAttachmentsRequest;
@ -14,7 +13,6 @@ import java.util.NoSuchElementException;
import java.util.Set;
/**
* 实例网络操作类.
* @author LamGC
*/
public class InstanceNetwork {
@ -24,7 +22,7 @@ public class InstanceNetwork {
InstanceNetwork(ComputeInstance instance) {
this.instance = instance;
this.vcnClient = instance.getFromAccount().clients().network();
this.vcnClient = new VirtualNetworkClient(this.instance.getAuthProvider());
}
/**
@ -57,24 +55,8 @@ public class InstanceNetwork {
.build()
);
return listVnicAttachments.getItems();
return listVnicAttachments.getItems().stream().toList();
}
/**
* 获取实例的主要 VNIC(虚拟网络接口)
* @return 返回实例的主要 VNIC 对象.
* @throws NoSuchElementException 当找不到主要 VNIC 时抛出该异常.
*/
public Vnic getPrimaryVnic() {
for (VnicAttachment vnicAttachment : listVnicAttachments()) {
GetVnicResponse vnic = vcnClient.getVnic(GetVnicRequest.builder()
.vnicId(vnicAttachment.getVnicId())
.build());
if (vnic.getVnic().getIsPrimary()) {
return vnic.getVnic();
}
}
throw new NoSuchElementException("Primary vnic not found.");
}
}

View File

@ -1,7 +1,7 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import net.lamgc.oracle.sentry.common.io.InputStreamWrapper;
import net.lamgc.oracle.sentry.common.io.OutputStreamWrapper;
import net.lamgc.oracle.sentry.common.InputStreamWrapper;
import net.lamgc.oracle.sentry.common.OutputStreamWrapper;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannelEvent;
@ -54,11 +54,9 @@ public final class CommandExecSession implements Closeable {
/**
* 等待程序执行完毕.
* @param timeout 超时时间, 0 为无限等待(单位: 毫秒).
* @return 如果在超时时间内返回, 返回 {@code true}, 超时返回 {@code false}.
*/
public boolean waitFor(long timeout) {
return !channelExec.waitFor(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.EXIT_SIGNAL), timeout)
.contains(ClientChannelEvent.TIMEOUT);
public void waitFor(long timeout) {
channelExec.waitFor(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.EXIT_SIGNAL), timeout);
}
/**

View File

@ -1,45 +0,0 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.server.forward.ForwardingFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 已配置转发过滤器.
* <p> 根据应用配置, 选择是否允许转发指定类型的流量.
* @author LamGC
*/
@Component("sentry.script.ssh.forwarding.filter")
public class ConfiguredForwardingFilter implements ForwardingFilter {
@Value("oracle.ssh.forwarding.X11.enable")
private String x11Enabled;
@Value("oracle.ssh.forwarding.tcp.enable")
private String tcpEnabled;
@Value("oracle.ssh.forwarding.agent.enable")
private String agentEnabled;
@Override
public boolean canForwardAgent(Session session, String requestType) {
return Boolean.parseBoolean(agentEnabled);
}
@Override
public boolean canListen(SshdSocketAddress address, Session session) {
return Boolean.parseBoolean(tcpEnabled);
}
@Override
public boolean canConnect(Type type, SshdSocketAddress address, Session session) {
return Boolean.parseBoolean(tcpEnabled);
}
@Override
public boolean canForwardX11(Session session, String requestType) {
return Boolean.parseBoolean(x11Enabled);
}
}

View File

@ -1,7 +1,6 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import com.google.common.base.Strings;
import net.lamgc.oracle.sentry.Constants;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.future.AuthFuture;
@ -41,15 +40,12 @@ public class InstanceSsh implements AutoCloseable {
this.authInfo = Objects.requireNonNull(authInfo);
sshClient = SshClient.setUpDefaultClient();
sshClient.setForwardingFilter(Constants.instance.getForwardingFilter());
sshClient.setServerKeyVerifier(new OracleInstanceServerKeyVerifier(instance, authInfo));
if (authInfo instanceof PublicKeyAuthInfo info) {
FileKeyPairProvider fileKeyPairProvider = new FileKeyPairProvider(info.getPrivateKeyPath().toPath());
sshClient.setKeyIdentityProvider(new FileKeyPairProvider(info.getPrivateKeyPath().toPath()));
if (!Strings.isNullOrEmpty(info.getKeyPassword())) {
fileKeyPairProvider.setPasswordFinder(FilePasswordProvider.of(info.getKeyPassword()));
log.info("已设置密钥解密密码.");
sshClient.setFilePasswordProvider(FilePasswordProvider.of(info.getKeyPassword()));
}
sshClient.setKeyIdentityProvider(fileKeyPairProvider);
} else if (authInfo instanceof PasswordAuthInfo info) {
sshClient.addPasswordIdentity(info.getPassword());
} else {

View File

@ -1,12 +1,10 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import net.lamgc.oracle.sentry.common.LazyLoader;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.common.SftpConstants;
import org.apache.sshd.sftp.common.SftpException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NoSuchFileException;
import java.util.HashSet;
@ -32,7 +30,6 @@ public class SftpSession implements Closeable {
}
private final SftpClient sftpClient;
private final LazyLoader<String> userHome;
/**
* 创建 Sftp 会话.
@ -40,20 +37,6 @@ public class SftpSession implements Closeable {
*/
SftpSession(SftpClient sftpClient) {
this.sftpClient = sftpClient;
this.userHome = new LazyLoader<>(() -> {
try {
CommandExecSession echoHOME = new CommandExecSession(sftpClient.getSession().createExecChannel("echo $HOME"));
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
echoHOME.setOut(outBuffer);
echoHOME.exec();
if (echoHOME.exitCode() != null && echoHOME.exitCode() == 0) {
return outBuffer.toString(StandardCharsets.UTF_8).trim();
}
throw new IOException("Command execution failed.(ExitCode: " + echoHOME.exitCode() + ")");
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
@ -264,17 +247,4 @@ public class SftpSession implements Closeable {
sftpClient.close();
}
/**
* 获取用户主目录.
* @return 返回用户主目录, 例如: "/root", 末尾无符号.
* @throws IOException 如果操作失败, 则抛出异常.
*/
public String getUserHome() throws IOException {
try {
return userHome.getInstance();
} catch (RuntimeException e) {
throw (IOException) e.getCause();
}
}
}

View File

@ -4,7 +4,7 @@ import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@ -1,8 +1,6 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.sftp.client.SftpClientFactory;
import java.io.Closeable;
@ -47,35 +45,6 @@ public class SshSession implements Closeable {
return new SftpSession(factory.createSftpClient(clientSession));
}
/**
* 创建本地 TCP 转发隧道.
* <p> 该隧道为方向为 "本地-&gt;远端" (本地发起连接转发至远端端口).
* @param localPort 本地监听端口.
* @param remotePort 远端目标端口.
* @return 返回 TCP 转发通道对象, 可获取通道信息和关闭通道.
* @throws IOException 当操作失败时抛出该异常.
*/
public TcpForwardingChannel createLocalTcpForwarding(int localPort, int remotePort) throws IOException {
ExplicitPortForwardingTracker tracker = clientSession
.createLocalPortForwardingTracker(localPort, new SshdSocketAddress(remotePort));
return new TcpForwardingChannel(tracker);
}
/**
* 创建远端 TCP 转发隧道.
* <p> 该隧道为方向为 "本地&lt;-远端" (远端服务器发起连接转发至本地端口).
* @param remotePort 远端监听端口号, 该端口为远端服务连接转发的端口号.
* @param localPort 本地连接端口号, 该端口为本地服务端的端口号.
* @return 返回 Tcp 转发通道对象, 用于管理转发通道.
* @throws IOException 当操作失败时抛出异常.
*/
public TcpForwardingChannel createRemoteTcpForwarding(int remotePort, int localPort) throws IOException {
ExplicitPortForwardingTracker tracker =
clientSession.createRemotePortForwardingTracker(
new SshdSocketAddress(remotePort), new SshdSocketAddress(localPort));
return new TcpForwardingChannel(tracker);
}
/**
* 关闭 SSH 连接会话, 该连接会话所属的其他会话将会一同被关闭.
* @throws IOException 关闭失败时抛出异常,

View File

@ -1,63 +0,0 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
* TCP 隧道.
* <p> 可通过该对象管理隧道.
* @author LamGC
*/
public class TcpForwardingChannel implements AutoCloseable {
private final ExplicitPortForwardingTracker tracker;
TcpForwardingChannel(ExplicitPortForwardingTracker tracker) {
this.tracker = tracker;
}
/**
* 是否为本地转发.
* @return 如果是本地转发, 则为 {@code true}, 否则返回 {@code false}, 代表远端转发.
*/
public boolean isLocalForwarding() {
return tracker.isLocalForwarding();
}
/**
* 隧道是否已打开.
* @return 如果已经打开, 返回 {@code true}, 否则返回 {@code false} 表示已关闭.
*/
public boolean isOpen() {
return tracker.isOpen();
}
/**
* 获取本地地址.
* @return 获取本地连接地址.
*/
public SshdSocketAddress getLocalAddress() {
return tracker.getLocalAddress();
}
/**
* 获取远端地址.
* @return 获取远端地址.
*/
public SshdSocketAddress getRemoteAddress() {
return tracker.getRemoteAddress();
}
/**
* 获取监听绑定地址.
* @return 返回监听绑定地址.
*/
public SshdSocketAddress getBoundAddress() {
return tracker.getBoundAddress();
}
@Override
public void close() throws Exception {
tracker.close();
}
}

View File

@ -1,13 +0,0 @@
package net.lamgc.oracle.sentry.script;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
public class BuiltinComponentExtension implements ScriptComponentExtension{
@Override
public void configureScriptComponents(ScriptComponents components) {
components.addComponentObject("HTTP", new ScriptHttpClient(HttpClientBuilder.create()
.build()));
components.addComponentFactory(new ScriptLoggerFactory());
}
}

View File

@ -1,18 +0,0 @@
package net.lamgc.oracle.sentry.script;
/**
* 脚本组件扩展.
* <p> 通过实现该接口, 可在脚本加载前设置组件对象, 为脚本提供更多功能.
* <p> 实现接口后, 需按 SPI 方式添加实现.
* @author LamGC
*/
public interface ScriptComponentExtension {
/**
* 配置脚本组件.
* <p> 在方法中为组件集合添加组件.
* @param components 脚本组件集合.
*/
void configureScriptComponents(final ScriptComponents components);
}

View File

@ -1,22 +0,0 @@
package net.lamgc.oracle.sentry.script;
/**
* 脚本组件工厂.
* @author LamGC
*/
public interface ScriptComponentFactory<T> {
/**
* 创建并获取实例.
* @param info 脚本信息.
* @return 返回对象.
*/
T getInstance(ScriptInfo info);
/**
* 对象属性名.
* @return 返回建议的对象属性名, {@link ScriptLoader} 并不一定遵守.
*/
String getPropertyName();
}

View File

@ -1,8 +1,7 @@
package net.lamgc.oracle.sentry.script;
import com.google.common.base.Strings;
import java.util.*;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
/**
* 脚本组件集合.
@ -10,49 +9,9 @@ import java.util.*;
* <p> 后续可能会改成用 {@link javax.script.Bindings} 之类的.
* @author LamGC
*/
public final class ScriptComponents {
private final ScriptComponents parent;
private final Map<String, Object> componentObjects = new HashMap<>();
private final Set<ScriptComponentFactory<?>> factories = new HashSet<>();
public ScriptComponents() {
this.parent = null;
}
public ScriptComponents(ScriptComponents parent) {
this.parent = parent;
}
public void addComponentObject(String componentName, Object componentObject) {
if (Strings.isNullOrEmpty(componentName)) {
throw new NullPointerException("The component name is null or empty.");
} else if (componentObject == null) {
throw new NullPointerException("ComponentObject is null");
} else if (componentObjects.containsKey(componentName)) {
throw new IllegalArgumentException("The corresponding object already exists for the component name.");
}
componentObjects.put(componentName, componentObject);
}
public void addComponentFactory(ScriptComponentFactory<?> factory) {
Objects.requireNonNull(factory);
factories.add(factory);
}
public Map<String, Object> getComponentObjects() {
Map<String, Object> componentObjects = this.parent == null ?
new HashMap<>() : new HashMap<>(this.parent.getComponentObjects());
componentObjects.putAll(this.componentObjects);
return Collections.unmodifiableMap(componentObjects);
}
public Set<ScriptComponentFactory<?>> getScriptComponentFactories() {
Set<ScriptComponentFactory<?>> factories = this.parent == null ?
new HashSet<>() : new HashSet<>(this.parent.factories);
factories.addAll(this.factories);
return Collections.unmodifiableSet(factories);
}
public final record ScriptComponents(
ScriptHttpClient HTTP,
ComputeInstanceManager InstanceManager
) {
}

View File

@ -1,98 +0,0 @@
package net.lamgc.oracle.sentry.script;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 脚本日志记录器工厂.
* <p> 通过 CGLIB 无缝为脚本设置 {@link Marker} 以将脚本日志输出到特定文件中.
* @author LamGC
*/
public class ScriptLoggerFactory implements ScriptComponentFactory<Logger> {
private final static Marker SCRIPT_MARKER = MarkerFactory.getMarker("Script");
@Override
public Logger getInstance(ScriptInfo info) {
Logger realLogger = LoggerFactory.getLogger(info.getGroup() + ":" + info.getName());
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Logger.class);
enhancer.setCallback(new LoggerProxyImpl(realLogger));
return (Logger) enhancer.create();
}
@Override
public String getPropertyName() {
return "Log";
}
private static class LoggerProxyImpl implements MethodInterceptor {
private final static Set<String> PROXY_METHOD_NAMES = Set.of(
"trace", "debug", "info", "warn", "error",
"isTraceEnabled",
"isDebugEnabled",
"isInfoEnabled",
"isWarnEnabled",
"isErrorEnabled"
);
private final static ConcurrentHashMap<Method, Method> METHOD_CACHE_MAP =
new ConcurrentHashMap<>(PROXY_METHOD_NAMES.size(), 1);
private final Logger targetLog;
private final Class<? extends Logger> logClass;
public LoggerProxyImpl(Logger targetLog) {
this.targetLog = targetLog;
logClass = targetLog.getClass();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (METHOD_CACHE_MAP.contains(method)) {
return METHOD_CACHE_MAP.get(method).invoke(targetLog, insertParameterToArray(args, SCRIPT_MARKER));
} else if (PROXY_METHOD_NAMES.contains(method.getName())) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 0 && !Marker.class.isAssignableFrom(types[0])) {
Class<?>[] realMethodParamTypes = insertTypeToArray(types, Marker.class);
Method realMethod = logClass.getDeclaredMethod(method.getName(), realMethodParamTypes);
METHOD_CACHE_MAP.put(method, realMethod);
realMethod.invoke(targetLog, insertParameterToArray(args, SCRIPT_MARKER));
return null;
}
}
return proxy.invoke(targetLog, args);
}
public Object[] insertParameterToArray(Object[] arr, Object newElement) {
if (arr.length == 0) {
return new Object[] {newElement};
}
Object[] newArr = new Object[arr.length + 1];
newArr[0] = newElement;
System.arraycopy(arr, 0, newArr, 1, arr.length);
return newArr;
}
public Class<?>[] insertTypeToArray(Class<?>[] arr, Class<?> newElement) {
if (arr.length == 0) {
return new Class<?>[] {newElement};
}
Class<?>[] newArr = new Class<?>[arr.length + 1];
newArr[0] = newElement;
System.arraycopy(arr, 0, newArr, 1, arr.length);
return newArr;
}
}
}

View File

@ -66,13 +66,6 @@ public final class ScriptManager {
} else {
continue;
}
ScriptInfo scriptInfo = loader.getScriptInfo(script);
if (scriptInfo == null) {
log.warn("脚本加载成功, 但加载器没有返回脚本信息, 该脚本已放弃.");
return false;
}
scripts.put(scriptInfo, script);
return true;
} catch (Exception e) {
log.error("脚本加载时发生异常.(Loader: {}, Path: {})\n{}",
loader.getClass().getName(),
@ -80,6 +73,13 @@ public final class ScriptManager {
Throwables.getStackTraceAsString(e));
throw new InvocationTargetException(e);
}
ScriptInfo scriptInfo = loader.getScriptInfo(script);
if (scriptInfo == null) {
log.warn("脚本加载成功, 但加载器没有返回脚本信息, 该脚本已放弃.");
return false;
}
scripts.put(scriptInfo, script);
return true;
}
return false;
}

View File

@ -2,9 +2,10 @@ package net.lamgc.oracle.sentry.script.groovy;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.Script;
import net.lamgc.oracle.sentry.script.ScriptInfo;
import net.lamgc.oracle.sentry.script.groovy.trigger.GroovyTrigger;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
/**
@ -15,28 +16,20 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods;
public class GroovyDslDelegate implements Script {
private final GroovyScriptInfo scriptInfo = new GroovyScriptInfo();
private final GroovyScriptLoader scriptLoader;
/**
* 构建一个 DSL Delegate, 并传入可操作对象.
* @param scriptLoader 该脚本所属的加载器.
*/
public GroovyDslDelegate(GroovyScriptLoader scriptLoader) {
this.scriptLoader = scriptLoader;
public GroovyDslDelegate() {
}
/**
* 注册触发器.
* <p> 注意: 如果脚本尚未初始化完成, 将无法注册触发器, 可通过 {@link #isInitialed()} 检查是否已经完成初始化.
* @param triggerName 触发器名称.
* @param closure 待执行闭包.
*/
private void trigger(String triggerName, Closure<?> closure){
if (!scriptLoader.isInitialed(this)) {
return;
}
GroovyTrigger trigger = GroovyTriggerProvider.INSTANCE.getTriggerByName(triggerName);
DefaultGroovyMethods.with(trigger, closure);
DefaultGroovyMethods.with(GroovyTriggerProvider.INSTANCE.getTriggerByName(triggerName), closure);
}
/**
@ -51,12 +44,4 @@ public class GroovyDslDelegate implements Script {
public ScriptInfo getScriptInfo() {
return scriptInfo;
}
/**
* 检查脚本当前是否已经初始化完成.
* @return 如果脚本已经初始化, 本方法将返回 {@code true}.
*/
public final boolean isInitialed() {
return scriptLoader.isInitialed(this);
}
}

View File

@ -1,6 +1,5 @@
package net.lamgc.oracle.sentry.script.groovy;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
@ -9,7 +8,6 @@ import net.lamgc.oracle.sentry.script.Script;
import net.lamgc.oracle.sentry.script.ScriptComponents;
import net.lamgc.oracle.sentry.script.ScriptInfo;
import net.lamgc.oracle.sentry.script.ScriptLoader;
import net.lamgc.oracle.sentry.script.ScriptComponentFactory;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -17,10 +15,9 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
@ -33,8 +30,8 @@ public class GroovyScriptLoader implements ScriptLoader {
private final static Logger log = LoggerFactory.getLogger(GroovyScriptLoader.class);
private final GroovyClassLoader scriptClassLoader;
private final Map<Script, ScriptInfo> scriptInfoMap = new ConcurrentHashMap<>();
private final Set<Script> initialedScript = new HashSet<>();
/**
* 构造一个新的脚本加载器.
@ -61,17 +58,11 @@ public class GroovyScriptLoader implements ScriptLoader {
Constructor<? extends DelegatingScript> constructor =
scriptClass.asSubclass(DelegatingScript.class).getConstructor();
DelegatingScript newScriptObject = constructor.newInstance();
GroovyDslDelegate dslDelegate = new GroovyDslDelegate(this);
GroovyDslDelegate dslDelegate = new GroovyDslDelegate();
newScriptObject.setDelegate(dslDelegate);
newScriptObject.setBinding(createBinding(context));
newScriptObject.run();
ScriptInfo scriptInfo = dslDelegate.getScriptInfo();
if (!checkScriptInfo(scriptInfo)) {
return null;
}
initialedScript.add(dslDelegate);
newScriptObject.setBinding(createBinding(context, scriptInfo));
newScriptObject.run();
scriptInfoMap.put(dslDelegate, scriptInfo);
scriptInfoMap.put(dslDelegate, dslDelegate.getScriptInfo());
return dslDelegate;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
log.error("加载脚本时发生异常.(ScriptPath: {})\n{}", scriptFile.getAbsolutePath(), Throwables.getStackTraceAsString(e));
@ -84,44 +75,18 @@ public class GroovyScriptLoader implements ScriptLoader {
return scriptInfoMap.get(script);
}
/**
* 检查脚本是否已经初始化完毕.
* @param script 脚本对象.
* @return 如果已初始化完毕, 返回 {@code true}.
*/
public boolean isInitialed(GroovyDslDelegate script) {
return initialedScript.contains(script);
}
private static Binding createBinding(ScriptComponents components, ScriptInfo info) {
private static Binding createBinding(ScriptComponents components) {
Binding binding = new Binding();
components.getComponentObjects().forEach(binding::setProperty);
for (ScriptComponentFactory<?> factory : components.getScriptComponentFactories()) {
String componentName = factory.getPropertyName();
if (binding.hasVariable(componentName)) {
log.warn("脚本组件名发生冲突: 工厂 {} 所给定的组件名已经存在静态组件, 将跳过创建.", factory.getClass());
continue;
for (Field field : components.getClass().getDeclaredFields()) {
try {
String name = field.getName();
field.setAccessible(true);
Object o = field.get(components);
binding.setProperty(name, o);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Object componentObject = factory.getInstance(info);
binding.setProperty(componentName, componentObject);
}
return binding;
}
private static boolean checkScriptInfo(ScriptInfo info) {
if (Strings.isNullOrEmpty(info.getGroup())) {
log.warn("脚本信息缺少 {}, 跳过加载.", "Group");
return false;
}
if (Strings.isNullOrEmpty(info.getName())) {
log.warn("脚本信息缺少 {}, 跳过加载.", "Name");
return false;
}
if (Strings.isNullOrEmpty(info.getVersion())) {
log.warn("脚本信息缺少 {}, 跳过加载.", "Version");
return false;
}
return true;
}
}

View File

@ -56,7 +56,7 @@ public class GroovyTriggerProvider {
if (!triggerProviderMap.containsKey(triggerName.toLowerCase())) {
throw new NoSuchElementException("The specified trigger could not be found: " + triggerName);
}
return triggerProviderMap.get(triggerName.toLowerCase()).get();
return triggerProviderMap.get(triggerName).get();
}
}

View File

@ -17,9 +17,4 @@ public interface GroovyTrigger {
*/
void run(Closure<?> task);
/**
* 关闭触发器.
*/
void shutdown();
}

View File

@ -1,286 +0,0 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.oracle.bmc.core.ComputeClient;
import com.oracle.bmc.core.model.Instance;
import com.oracle.bmc.core.requests.GetInstanceRequest;
import groovy.lang.Closure;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicReference;
/**
* 实例状态监视触发器.
* <p> 通过轮询实例状态, 当状态为指定的状态时将触发回调执行操作.
* <p> 吐槽: 换个地方轮询(xs).
* @author LamGC
*/
@TriggerName("InstanceStateWatcher")
public class InstanceStateWatchTrigger implements GroovyTrigger {
private final static Logger log = LoggerFactory.getLogger(InstanceStateWatchTrigger.class);
private final static ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("Thread-InstanceStatePolling-%d")
.build();
private final Set<ComputeInstance> instances = new HashSet<>();
private final AtomicReference<ComputeInstanceManager> instanceManager = new AtomicReference<>();
private final AtomicReference<Thread> pollingThreadReference = new AtomicReference<>();
private final Set<Instance.LifecycleState> targetStates = new HashSet<>();
private long interval = 5000;
/**
* 设置 {@link ComputeInstanceManager}.
* <p> 设置计算实例管理器以实现部分功能.
* @param instanceManager 实例管理器.
*/
public void setInstanceManager(ComputeInstanceManager instanceManager) {
this.instanceManager.set(instanceManager);
}
/**
* 添加所有实例.
* <p> 调用前需设置实例管理器.
*/
public void allInstances() {
addInstance(instanceManager.get().getComputeInstances());
}
/**
* 添加需要监控的实例.
* <p> 调用前需设置实例管理器.
* @param instanceId 要添加的实例 Id.(注意不是实例名称.)
*/
public void addInstance(String instanceId) {
this.instances.add(instanceManager.get().getComputeInstanceById(instanceId));
}
/**
* 添加需要监控的实例.
* @param instance 实例对象.
*/
public void addInstance(ComputeInstance instance) {
this.instances.add(instance);
}
/**
* 添加需要监控的实例.
* @param instances 实例集合.
*/
public void addInstance(Set<ComputeInstance> instances) {
this.instances.addAll(instances);
}
/**
* 设置监控间隔.
* <p> 该间隔是每轮查询之间的间隔, 并非查询每个实例的间隔. Oracle 对该接口的请求限制很宽松, 且单个帐号的实例有限, 故不再设置查询间隔.
* @param interval 间隔时常, 单位: 毫秒.
*/
public void interval(long interval) {
this.interval = interval;
}
/**
* 要检查的状态.
* <p> 当实例处于指定状态时将触发回调.
* @param state 状态名.
*/
public void state(String state) {
targetStates.add(getState(state));
}
/**
* 要检查的状态.
* <p> 当实例处于指定状态时将触发回调.
* @param states 状态集合.
*/
public void state(String[] states) {
Set<Instance.LifecycleState> stateSet = new HashSet<>();
for (String state : states) {
stateSet.add(getState(state));
}
targetStates.addAll(stateSet);
}
/**
* 添加所有状态.
* <p> 所有状态都会触发回调函数.
*/
public void allStates() {
targetStates.addAll(Set.of(Instance.LifecycleState.values()));
}
/**
* 通过状态名获取对应的 {@link Instance.LifecycleState}.
* @param state 状态名.
* @return 返回对应值.
* @throws IllegalArgumentException 当无法获取指定状态对象时抛出该异常.
*/
private Instance.LifecycleState getState(String state) {
try {
return LifecycleStateMapping.valueOf(state.toUpperCase()).target;
} catch (IllegalArgumentException e) {
try {
return Instance.LifecycleState.valueOf(toFirstCharUpperCase(state));
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Invalid target state: " + state);
}
}
}
@Override
public synchronized void run(final Closure<?> callback) {
if (pollingThreadReference.get() != null) {
throw new IllegalStateException("Attempting to start multiple check threads repeatedly, which is not allowed.");
}
if (targetStates.isEmpty()) {
throw new IllegalArgumentException("The target status has not been set.");
}
if (instances.isEmpty()) {
log.warn("尚未设置任何需监视的实例, 已跳过对触发器的启动.", new IllegalStateException("未设置任何需要监视的实例."));
return;
}
Thread newPollingThread = THREAD_FACTORY.newThread(new PollingTask(instances, targetStates, callback, interval));
newPollingThread.start();
pollingThreadReference.set(newPollingThread);
}
@Override
public synchronized void shutdown() {
if (pollingThreadReference.get() == null) {
return;
}
// 合规的用于停止线程的方法无歧义
Thread pollingThread = pollingThreadReference.get();
pollingThread.interrupt();
pollingThreadReference.set(null);
}
/**
* 首字母大写.
* @param str 要处理的单词.
* @return 如果首字母非大写, 就转换成大写.
*/
private static String toFirstCharUpperCase(String str) {
char firstChar = str.charAt(0);
if (!Character.isUpperCase(firstChar)) {
return Character.toUpperCase(firstChar) + str.substring(1);
}
return str;
}
/**
* 轮询任务.
*/
private static class PollingTask implements Runnable {
private final Set<ComputeInstance> instances;
private final Set<Instance.LifecycleState> targetStates;
private final Closure<?> callback;
private final long interval;
private final Map<ComputeInstance, Instance.LifecycleState> lastStateMap = new ConcurrentHashMap<>();
private PollingTask(Set<ComputeInstance> instances, Set<Instance.LifecycleState> targetStates,
Closure<?> callback, long interval) {
this.instances = instances;
this.targetStates = targetStates;
this.callback = callback;
if (interval <= 0) {
throw new IllegalArgumentException("Interval cannot be less than or equal to 0.");
}
this.interval = interval;
}
@SuppressWarnings("BusyWait")
@Override
public void run() {
while (!Thread.interrupted()) {
try {
// 并非 BusyWait, 而是延迟操作.
Thread.sleep(interval);
} catch (InterruptedException e) {
break;
}
log.trace("正在开始新一轮实例状态检查...");
for (ComputeInstance instance : instances) {
ComputeClient computeClient = instance.getFromAccount().clients().compute();
Instance instanceInfo;
try {
instanceInfo = computeClient.getInstance(GetInstanceRequest.builder()
.instanceId(instance.getInstanceId())
.build()).getInstance();
} catch (Exception e) {
if (e.getCause() instanceof InterruptedException) {
Thread.currentThread().interrupt();
break;
}
if (log.isDebugEnabled()) {
log.error("获取实例状态失败.", e);
} else {
log.error("获取实例状态失败(详情请打开 Debug 级别): {}:{}", e.getClass().getName(), e.getMessage());
}
continue;
}
Instance.LifecycleState lastState = lastStateMap.get(instance);
if (targetStates.contains(instanceInfo.getLifecycleState()) &&
instanceInfo.getLifecycleState() != lastState) {
try {
callback.call(new InstanceStateChangeEvent(instance,
lastState != null ? lastState.name() : null,
instanceInfo.getLifecycleState().name()));
} catch (Exception e) {
log.error("实例状态事件处理时发生未捕获异常.", e);
}
}
lastStateMap.put(instance, instanceInfo.getLifecycleState());
}
log.trace("实例状态检查已结束.");
}
}
}
/**
* 一个 Oracle 实例状态枚举的映射枚举类.
*/
@SuppressWarnings({"AlibabaEnumConstantsMustHaveComment", "unused"})
private enum LifecycleStateMapping {
MOVING(Instance.LifecycleState.Moving),
PROVISIONING(Instance.LifecycleState.Provisioning),
RUNNING(Instance.LifecycleState.Running),
STARTING(Instance.LifecycleState.Starting),
STOPPING(Instance.LifecycleState.Stopping),
STOPPED(Instance.LifecycleState.Stopped),
CREATING_IMAGE(Instance.LifecycleState.CreatingImage),
TERMINATING(Instance.LifecycleState.Terminating),
TERMINATED(Instance.LifecycleState.Terminated);
private final Instance.LifecycleState target;
LifecycleStateMapping(Instance.LifecycleState target) {
this.target = target;
}
}
/**
* 实例状态变更事件.
* @param instance 相关实例.
* @param oldState 旧状态, 当监视器第一次查询即触发时, 该值为 {@code null}.
* @param newState 新状态.
*/
public static record InstanceStateChangeEvent(
ComputeInstance instance,
String oldState,
String newState
) {}
}

View File

@ -2,6 +2,7 @@ package net.lamgc.oracle.sentry.script.groovy.trigger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import groovy.lang.Closure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutorService;
@ -13,21 +14,17 @@ import java.util.concurrent.Executors;
*/
@TriggerName("once")
public class OnceTrigger implements GroovyTrigger {
private final static Logger log = LoggerFactory.getLogger(OnceTrigger.class);
private final static ExecutorService EXECUTOR = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder()
.setNameFormat("GroovyOnceExec-%d")
.setUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(OnceTrigger.class)
.error("脚本执行时发生未捕获异常.", e))
.setUncaughtExceptionHandler((t, e) -> log.error("脚本执行时发生未捕获异常.", e))
.build());
@Override
public void run(Closure<?> task) {
EXECUTOR.execute(task);
}
@Override
public void shutdown() {
// Nothing.
}
}

View File

@ -8,30 +8,25 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import java.util.concurrent.ScheduledFuture;
/**
* 时间触发器.
* <p> 通过设置 Cron 时间表达式, 可以达到定时触发的效果.
* @author LamGC
*/
@SuppressWarnings("unused")
@TriggerName("timer")
public class TimerTrigger implements GroovyTrigger {
private final static Logger log = LoggerFactory.getLogger(TimerTrigger.class);
private CronTrigger trigger;
private final static ThreadPoolTaskScheduler SCHEDULER = new ThreadPoolTaskScheduler();
static {
SCHEDULER.setPoolSize(Runtime.getRuntime().availableProcessors());
SCHEDULER.setThreadFactory(new ThreadFactoryBuilder()
.setNameFormat("Groovy-TimerTrigger-%d")
.build());
SCHEDULER.setErrorHandler(t -> triggerLog().error("脚本执行时发生异常.", t));
SCHEDULER.initialize();
SCHEDULER.setErrorHandler(t -> log.error("脚本执行时发生异常.", t));
}
private CronTrigger trigger;
private ScheduledFuture<?> future;
/**
* 设定定时时间.
* <p> 只允许在第一次执行时设置.
@ -44,42 +39,26 @@ public class TimerTrigger implements GroovyTrigger {
}
@Override
public synchronized void run(Closure<?> runnable) {
if (future != null) {
triggerLog().warn("脚本存在多个 run 代码块, 已忽略.");
return;
}
public void run(Closure<?> runnable) {
if (trigger == null) {
if (!triggerLog().isDebugEnabled()) {
triggerLog().warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
if (!log.isDebugEnabled()) {
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
} else {
triggerLog().warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
triggerLog().warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
}
return;
} else if (runnable == null) {
if (!triggerLog().isDebugEnabled()) {
triggerLog().warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
if (!log.isDebugEnabled()) {
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
} else {
triggerLog().warn("{} - 脚本尚未设置任务动作, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
triggerLog().warn("{} - 脚本尚未设置任务动作, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
}
return;
}
this.future = SCHEDULER.schedule(new TimerTaskRunnable(runnable), trigger);
}
@Override
public void shutdown() {
if (this.future != null) {
future.cancel(false);
}
}
private static Logger triggerLog() {
return LoggerFactory.getLogger(TimerTrigger.class);
SCHEDULER.schedule(new TimerTaskRunnable(runnable), trigger);
}
private static class TimerTaskRunnable implements Runnable {

View File

@ -1 +0,0 @@
net.lamgc.oracle.sentry.script.BuiltinComponentExtension

View File

@ -1,3 +1,2 @@
net.lamgc.oracle.sentry.script.groovy.trigger.OnceTrigger
net.lamgc.oracle.sentry.script.groovy.trigger.TimerTrigger
net.lamgc.oracle.sentry.script.groovy.trigger.InstanceStateWatchTrigger

View File

@ -14,14 +14,3 @@ oracle:
# 首次连接认证策略
# 支持 confirm(询问) accept(接受) reject(拒绝)
authenticationPolicy: 'confirm'
# SSH 转发设定
forwarding:
# X11 转发
X11:
enable: false
# SSH-Agent 转发
agent:
enable: false
# TCP 转发
tcp:
enable: true

View File

@ -2,10 +2,10 @@
<configuration debug="false">
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="net.lamgc.oracle.sentry.common.logging.NoMarkerFilter"/>
<filter class="net.lamgc.oracle.sentry.common.logging.LevelRangeFilter">
<minLevel>DEBUG</minLevel>
<maxLevel>INFO</maxLevel>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{HH:mm:ss.SSS} %5level][%logger][%thread]: %msg%n</pattern>
@ -13,7 +13,6 @@
</appender>
<appender name="stderr" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<filter class="net.lamgc.oracle.sentry.common.logging.NoMarkerFilter"/>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
@ -24,7 +23,6 @@
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/latest.log</file>
<filter class="net.lamgc.oracle.sentry.common.logging.NoMarkerFilter"/>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>DENY</onMatch>
@ -39,63 +37,11 @@
</rollingPolicy>
</appender>
<appender name="stdout_script" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<filter class="net.lamgc.oracle.sentry.common.logging.MarkerFilter">
<markerName>Script</markerName>
</filter>
<filter class="net.lamgc.oracle.sentry.common.logging.LevelRangeFilter">
<minLevel>DEBUG</minLevel>
<maxLevel>INFO</maxLevel>
</filter>
<encoder>
<pattern>[%d{HH:mm:ss.SSS} %5level][Script][%logger]: %msg%n</pattern>
</encoder>
</appender>
<appender name="stderr_script" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<filter class="net.lamgc.oracle.sentry.common.logging.MarkerFilter">
<markerName>Script</markerName>
</filter>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<encoder>
<pattern>[%d{HH:mm:ss.SSS} %5level][Script][%logger]: %msg%n</pattern>
</encoder>
</appender>
<appender name="logFile_script" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/latest-script.log</file>
<filter class="net.lamgc.oracle.sentry.common.logging.MarkerFilter">
<markerName>Script</markerName>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>[%d{HH:mm:ss.SSS} %5level][Script][%logger]: %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./logs/run-script-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<logger name="com.oracle.bmc" level="WARN"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.apache.http" level="INFO"/>
<logger name="org.apache.sshd" level="INFO"/>
<logger name="com.oracle.bmc.http.ApacheConfigurator" level="ERROR"/>
<root level="INFO">
<root>
<appender-ref ref="stdout" />
<appender-ref ref="stderr" />
<appender-ref ref="logFile" />
<appender-ref ref="stdout_script" />
<appender-ref ref="stderr_script" />
<appender-ref ref="logFile_script" />
</root>
</configuration>

View File

@ -1,63 +0,0 @@
package net.lamgc.oracle.sentry.common;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
class LazyLoaderTest {
@Test
@SuppressWarnings("rawtypes")
public void lazyLoadTest() throws NoSuchFieldException, IllegalAccessException {
LazyLoader<Object> loader = new LazyLoader<>(Object::new);
Field field = LazyLoader.class.getDeclaredField("object");
field.setAccessible(true);
AtomicReference reference = (AtomicReference) field.get(loader);
assertNotNull(reference);
assertNull(reference.get());
Object instance = loader.getInstance();
assertEquals(reference.get(), instance);
assertEquals(reference.get(), loader.getInstance());
assertEquals(instance, loader.getInstance());
}
@Test
@SuppressWarnings("StatementWithEmptyBody")
public void multiThreadAccessTest() {
class Singleton {
private final static AtomicInteger constructNum = new AtomicInteger(0);
public Singleton() {
if (constructNum.incrementAndGet() > 1) {
fail("Multiple instances were generated.");
}
}
}
final LazyLoader<Singleton> loader = new LazyLoader<>(Singleton::new);
AtomicBoolean start = new AtomicBoolean(false);
int threadNum = Runtime.getRuntime().availableProcessors();
List<Thread> threads = new ArrayList<>(threadNum);
for (int i = 0; i < threadNum; i++) {
Thread thread = new Thread(() -> {
while (!start.get()) {
}
assertNotNull(loader.getInstance());
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
start.set(true);
}
}

View File

@ -1,57 +0,0 @@
package net.lamgc.oracle.sentry.common.retry;
import org.junit.jupiter.api.Test;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class RetryerTest {
@Test
void successTest() throws RetryFailedException, ExecutionException, InterruptedException {
final Object obj = new Object();
Retryer<Object> retryer = Retryer.builder(() -> obj).create();
assertEquals(obj, retryer.execute());
assertEquals(obj, retryer.executeAsync().get());
}
@Test
void failedTest() {
assertThrows(RetryFailedException.class, () -> {
Retryer<Object> retryer = Retryer.builder(() -> {
throw new RuntimeException();
}).create();
retryer.execute();
});
}
@Test
void retryNumberTest() {
final int retryNumber = new Random().nextInt(9) + 1;
final AtomicInteger retryCounter = new AtomicInteger(-1);
Retryer<Object> retryer = Retryer.builder(() -> {
retryCounter.incrementAndGet();
throw new RuntimeException();
}).retryNumber(retryNumber).create();
try {
retryer.execute();
} catch (RetryFailedException e) {
e.printStackTrace();
}
assertEquals(retryNumber, retryCounter.get());
}
@Test
void checkerTest() {
Retryer<Object> retryer = Retryer.builder(() -> null)
.retryIfReturnNull()
.create();
assertThrows(RetryFailedException.class, retryer::execute);
}
}

View File

@ -1,7 +1,6 @@
package net.lamgc.oracle.sentry.script;
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.jupiter.api.Test;
@ -13,7 +12,8 @@ class ScriptManagerTest {
@Test
public void loadScriptTest() {
ScriptManager manager = new ScriptManager(new File("./run/scripts"),
new ScriptComponents());
new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create().build()),
new ComputeInstanceManager()));
manager.loadScripts();

View File

@ -7,9 +7,4 @@ public abstract class BaseTestTrigger implements GroovyTrigger {
public void run(Closure<?> task) {
throw new UnsupportedOperationException("Unavailable trigger.");
}
@Override
public void shutdown() {
// Do nothing.
}
}