mirror of
https://github.com/LamGC/Oracle-Sentry.git
synced 2025-04-29 22:27:34 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
626ce88565 | |||
1cdc15a05f | |||
bded118c03 | |||
4ca9139b3e | |||
b84b2d1ef6 | |||
43a8ac4026 | |||
e6ff28e077 | |||
7a008848db | |||
e55dde09fd | |||
2f98d1f91d | |||
18d8bb1ef2 | |||
652dabedef | |||
8c51bac6be | |||
9d5c91d5b4 | |||
ab69189d5b | |||
2006097d5f | |||
7dcbcc3270 | |||
a128748b4f | |||
b8b2268d2e | |||
d6738d635c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -31,3 +31,4 @@ hs_err_pid*
|
||||
|
||||
# Idea Editor
|
||||
/.idea
|
||||
out/
|
||||
|
@ -12,6 +12,10 @@
|
||||
|
||||
本程序为脚本提供 SSH 客户端(使用 [apache/Mina-sshd](https://github.com/apache/mina-sshd) ),协助脚本通过 SSH 管理服务器。
|
||||
|
||||
### 关于更新慢的说明 ###
|
||||
这个项目一直在更新(只有仓库存档了才会说不更),有什么不足的,
|
||||
或者想添加什么功能的可以发 issue,最近本人比较忙,可能没那么及时,但本项目肯定是不会弃坑的 :D
|
||||
|
||||
## Docs ##
|
||||
[中文(简体)](./docs/cn/Index.md)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
//file:noinspection GradlePackageUpdate SpringBoot 的版本由插件管理.
|
||||
//file:noinspection GroovyAssignabilityCheck
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '2.5.4'
|
||||
@ -6,7 +7,7 @@ plugins {
|
||||
id 'jacoco'
|
||||
}
|
||||
|
||||
group 'net.lamgc.oracle'
|
||||
group 'net.lamgc'
|
||||
version '0.2.0'
|
||||
compileJava.sourceCompatibility = JavaVersion.VERSION_16
|
||||
|
||||
|
@ -3,11 +3,10 @@ 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 net.lamgc.oracle.sentry.script.ScriptComponents;
|
||||
import net.lamgc.oracle.sentry.script.ScriptLoggerFactory;
|
||||
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;
|
||||
@ -23,6 +22,7 @@ import javax.annotation.PostConstruct;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
/**
|
||||
* @author LamGC
|
||||
@ -95,18 +95,29 @@ class ApplicationInitiation {
|
||||
@Bean("sentry.script.manager")
|
||||
@Autowired
|
||||
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager, OracleAccountManager accountManager) {
|
||||
ScriptComponents context = new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create()
|
||||
.build()),
|
||||
instanceManager,
|
||||
new ScriptLoggerFactory(),
|
||||
accountManager
|
||||
);
|
||||
ScriptComponents components = new ScriptComponents();
|
||||
components.addComponentObject("InstanceManager", instanceManager);
|
||||
components.addComponentObject("AccountManager", accountManager);
|
||||
|
||||
ScriptManager manager = new ScriptManager(new File(scriptsLocation), context);
|
||||
configureScriptComponentsFromExtension(components);
|
||||
|
||||
ScriptManager manager = new ScriptManager(new File(scriptsLocation), components);
|
||||
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"})
|
||||
|
@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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;
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.lamgc.oracle.sentry.common.retry;
|
||||
|
||||
/**
|
||||
* 重试延迟器.
|
||||
* <p> 用于决定每次重试的间隔时间, 可根据重试次数调整间隔时常, 以避免频繁执行影响性能.
|
||||
* @author LamGC
|
||||
*/
|
||||
public interface RetryDelayer {
|
||||
|
||||
/**
|
||||
* 获取下一次重试延迟时间.
|
||||
* @param currentRetryCount 当前重试次数, 如果第一次重试失败, 则本参数为 0.
|
||||
* @return 返回延迟时间.
|
||||
*/
|
||||
long nextDelayTime(int currentRetryCount);
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.lamgc.oracle.sentry.common.retry;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RetryExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理异常, 并指示是否继续重试.
|
||||
* @param e 异常对象.
|
||||
* @return 如果可以继续重试, 返回 {@code true}.
|
||||
*/
|
||||
boolean handle(Exception e);
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
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;
|
||||
|
||||
}
|
177
src/main/java/net/lamgc/oracle/sentry/common/retry/Retryer.java
Normal file
177
src/main/java/net/lamgc/oracle/sentry/common/retry/Retryer.java
Normal file
@ -0,0 +1,177 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -57,6 +61,30 @@ public final class OracleAccount {
|
||||
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 客户端集.
|
||||
|
@ -7,6 +7,9 @@ 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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -18,7 +21,8 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -55,23 +59,39 @@ public final class OracleAccountManager {
|
||||
if (configFiles == null) {
|
||||
throw new IOException("Unable to access the specified directory: " + directory.getCanonicalPath());
|
||||
}
|
||||
int loadedCount = 0;
|
||||
AtomicInteger loadedCount = new AtomicInteger();
|
||||
for (File configFile : configFiles) {
|
||||
try {
|
||||
Retryer<OracleAccount> retryer = Retryer.builder(() -> {
|
||||
OracleAccount account = loadFromConfigFile(configFile);
|
||||
if (account == null) {
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
loadedCount ++;
|
||||
log.info("已成功加载身份配置文件.\n\tUserId: {}\n\tUsername: {}\n\tPath: {}",
|
||||
account.id(),
|
||||
account.name(),
|
||||
configFile.getCanonicalPath());
|
||||
} catch (Exception e) {
|
||||
log.error("加载身份配置文件时发生异常.(Path: {})\n{}", configFile.getCanonicalPath(), Throwables.getStackTraceAsString(e));
|
||||
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) {
|
||||
continue;
|
||||
} else if (e instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
log.error("加载身份配置文件时发生异常.(Path: {})\n{}", configFile.getCanonicalPath(), Throwables.getStackTraceAsString(e.getCause()));
|
||||
}
|
||||
}
|
||||
return loadedCount;
|
||||
return loadedCount.get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,6 @@ 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.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.account.OracleAccount;
|
||||
import net.lamgc.oracle.sentry.oci.compute.ssh.InstanceSsh;
|
||||
import net.lamgc.oracle.sentry.oci.compute.ssh.SshAuthInfo;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.lamgc.oracle.sentry;
|
||||
package net.lamgc.oracle.sentry.oci.compute;
|
||||
|
||||
import com.oracle.bmc.core.model.Instance;
|
||||
import com.oracle.bmc.core.requests.ListInstancesRequest;
|
||||
@ -7,8 +7,9 @@ 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;
|
||||
@ -25,6 +26,8 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public final class ComputeInstanceManager {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ComputeInstanceManager.class);
|
||||
|
||||
private final Map<String, ComputeInstance> instanceMap = new ConcurrentHashMap<>();
|
||||
private SshAuthIdentityProvider sshIdentityProvider;
|
||||
|
||||
@ -34,8 +37,10 @@ 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 认证配置提供器已初始化完成.");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,13 +96,15 @@ public final class ComputeInstanceManager {
|
||||
.compartmentId(compartmentId)
|
||||
.build());
|
||||
for (Instance instance : listInstances.getItems()) {
|
||||
if (instance.getLifecycleState() == Instance.LifecycleState.Terminated) {
|
||||
if (instance.getLifecycleState() == Instance.LifecycleState.Terminated ||
|
||||
instance.getLifecycleState() == Instance.LifecycleState.Terminating) {
|
||||
log.debug("实例 {} 状态为 {}, 不添加该实例.", instance.getId(), instance.getLifecycleState());
|
||||
continue;
|
||||
}
|
||||
ComputeInstance computeInstance = new ComputeInstance(this, instance.getId(),
|
||||
compartmentId, instance.getImageId(), account);
|
||||
|
||||
addComputeInstance(computeInstance);
|
||||
log.debug("已为用户 {} 添加计算实例: {}", account.id(), instance.getId());
|
||||
addCount ++;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
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;
|
||||
@ -56,8 +57,24 @@ public class InstanceNetwork {
|
||||
.build()
|
||||
);
|
||||
|
||||
return listVnicAttachments.getItems().stream().toList();
|
||||
return listVnicAttachments.getItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例的主要 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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -54,9 +54,11 @@ public final class CommandExecSession implements Closeable {
|
||||
/**
|
||||
* 等待程序执行完毕.
|
||||
* @param timeout 超时时间, 0 为无限等待(单位: 毫秒).
|
||||
* @return 如果在超时时间内返回, 返回 {@code true}, 超时返回 {@code false}.
|
||||
*/
|
||||
public void waitFor(long timeout) {
|
||||
channelExec.waitFor(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.EXIT_SIGNAL), timeout);
|
||||
public boolean waitFor(long timeout) {
|
||||
return !channelExec.waitFor(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.EXIT_SIGNAL), timeout)
|
||||
.contains(ClientChannelEvent.TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,10 +44,12 @@ public class InstanceSsh implements AutoCloseable {
|
||||
sshClient.setForwardingFilter(Constants.instance.getForwardingFilter());
|
||||
sshClient.setServerKeyVerifier(new OracleInstanceServerKeyVerifier(instance, authInfo));
|
||||
if (authInfo instanceof PublicKeyAuthInfo info) {
|
||||
sshClient.setKeyIdentityProvider(new FileKeyPairProvider(info.getPrivateKeyPath().toPath()));
|
||||
FileKeyPairProvider fileKeyPairProvider = new FileKeyPairProvider(info.getPrivateKeyPath().toPath());
|
||||
if (!Strings.isNullOrEmpty(info.getKeyPassword())) {
|
||||
sshClient.setFilePasswordProvider(FilePasswordProvider.of(info.getKeyPassword()));
|
||||
fileKeyPairProvider.setPasswordFinder(FilePasswordProvider.of(info.getKeyPassword()));
|
||||
log.info("已设置密钥解密密码.");
|
||||
}
|
||||
sshClient.setKeyIdentityProvider(fileKeyPairProvider);
|
||||
} else if (authInfo instanceof PasswordAuthInfo info) {
|
||||
sshClient.addPasswordIdentity(info.getPassword());
|
||||
} else {
|
||||
|
@ -1,10 +1,12 @@
|
||||
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;
|
||||
@ -30,6 +32,7 @@ public class SftpSession implements Closeable {
|
||||
}
|
||||
|
||||
private final SftpClient sftpClient;
|
||||
private final LazyLoader<String> userHome;
|
||||
|
||||
/**
|
||||
* 创建 Sftp 会话.
|
||||
@ -37,6 +40,20 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,4 +264,17 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.compute.ComputeInstance;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -0,0 +1,13 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.lamgc.oracle.sentry.script;
|
||||
|
||||
/**
|
||||
* 脚本组件扩展.
|
||||
* <p> 通过实现该接口, 可在脚本加载前设置组件对象, 为脚本提供更多功能.
|
||||
* <p> 实现接口后, 需按 SPI 方式添加实现.
|
||||
* @author LamGC
|
||||
*/
|
||||
public interface ScriptComponentExtension {
|
||||
|
||||
/**
|
||||
* 配置脚本组件.
|
||||
* <p> 在方法中为组件集合添加组件.
|
||||
* @param components 脚本组件集合.
|
||||
*/
|
||||
void configureScriptComponents(final ScriptComponents components);
|
||||
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package net.lamgc.oracle.sentry.script;
|
||||
|
||||
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
|
||||
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 脚本组件集合.
|
||||
@ -10,11 +10,49 @@ import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||
* <p> 后续可能会改成用 {@link javax.script.Bindings} 之类的.
|
||||
* @author LamGC
|
||||
*/
|
||||
public final record ScriptComponents(
|
||||
ScriptHttpClient HTTP,
|
||||
ComputeInstanceManager InstanceManager,
|
||||
ScriptLoggerFactory loggerFactory,
|
||||
OracleAccountManager AccountManager
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,13 +66,6 @@ public final class ScriptManager {
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("脚本加载时发生异常.(Loader: {}, Path: {})\n{}",
|
||||
loader.getClass().getName(),
|
||||
scriptFile.getAbsolutePath(),
|
||||
Throwables.getStackTraceAsString(e));
|
||||
throw new InvocationTargetException(e);
|
||||
}
|
||||
ScriptInfo scriptInfo = loader.getScriptInfo(script);
|
||||
if (scriptInfo == null) {
|
||||
log.warn("脚本加载成功, 但加载器没有返回脚本信息, 该脚本已放弃.");
|
||||
@ -80,6 +73,13 @@ public final class ScriptManager {
|
||||
}
|
||||
scripts.put(scriptInfo, script);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("脚本加载时发生异常.(Loader: {}, Path: {})\n{}",
|
||||
loader.getClass().getName(),
|
||||
scriptFile.getAbsolutePath(),
|
||||
Throwables.getStackTraceAsString(e));
|
||||
throw new InvocationTargetException(e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ 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;
|
||||
@ -96,19 +95,15 @@ public class GroovyScriptLoader implements ScriptLoader {
|
||||
|
||||
private static Binding createBinding(ScriptComponents components, ScriptInfo info) {
|
||||
Binding binding = new Binding();
|
||||
for (Field field : components.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
String name = field.getName();
|
||||
field.setAccessible(true);
|
||||
Object o = field.get(components);
|
||||
if (o instanceof ScriptComponentFactory factory) {
|
||||
binding.setProperty(factory.getPropertyName(), factory.getInstance(info));
|
||||
} else {
|
||||
binding.setProperty(name, o);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
components.getComponentObjects().forEach(binding::setProperty);
|
||||
for (ScriptComponentFactory<?> factory : components.getScriptComponentFactories()) {
|
||||
String componentName = factory.getPropertyName();
|
||||
if (binding.hasVariable(componentName)) {
|
||||
log.warn("脚本组件名发生冲突: 工厂 {} 所给定的组件名已经存在静态组件, 将跳过创建.", factory.getClass());
|
||||
continue;
|
||||
}
|
||||
Object componentObject = factory.getInstance(info);
|
||||
binding.setProperty(componentName, componentObject);
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
@ -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).get();
|
||||
return triggerProviderMap.get(triggerName.toLowerCase()).get();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,286 @@
|
||||
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
|
||||
) {}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
net.lamgc.oracle.sentry.script.BuiltinComponentExtension
|
@ -1,2 +1,3 @@
|
||||
net.lamgc.oracle.sentry.script.groovy.trigger.OnceTrigger
|
||||
net.lamgc.oracle.sentry.script.groovy.trigger.TimerTrigger
|
||||
net.lamgc.oracle.sentry.script.groovy.trigger.InstanceStateWatchTrigger
|
||||
|
@ -79,7 +79,7 @@
|
||||
<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</fileNamePattern>
|
||||
<fileNamePattern>./logs/run-script-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
@ -87,8 +87,9 @@
|
||||
<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>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="stdout" />
|
||||
<appender-ref ref="stderr" />
|
||||
<appender-ref ref="logFile" />
|
||||
|
@ -0,0 +1,57 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package net.lamgc.oracle.sentry.script;
|
||||
|
||||
import net.lamgc.oracle.sentry.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.compute.ComputeInstanceManager;
|
||||
import net.lamgc.oracle.sentry.oci.account.OracleAccountManager;
|
||||
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
@ -13,8 +13,7 @@ class ScriptManagerTest {
|
||||
@Test
|
||||
public void loadScriptTest() {
|
||||
ScriptManager manager = new ScriptManager(new File("./run/scripts"),
|
||||
new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create().build()),
|
||||
new ComputeInstanceManager(), new ScriptLoggerFactory(), new OracleAccountManager()));
|
||||
new ScriptComponents());
|
||||
|
||||
manager.loadScripts();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user