40 Commits

Author SHA1 Message Date
800bb13c9e release: 更新版本至 0.1.0; 2021-08-20 19:49:55 +08:00
984576f5e1 fact: 添加 SSH 端口配置项.
添加 SSH 端口配置项以允许自定义 SSH 连接端口.
本次提交也补充了相应的测试项, 覆盖率 100%.
2021-08-20 18:23:45 +08:00
59f1f3e57a docs: 更新文档目录和文档文件名. 2021-08-20 16:44:28 +08:00
88a0440541 docs: 更新文档目录. 2021-08-20 16:41:36 +08:00
3fcd506f0c docs: 补充脚本编写文档. 2021-08-20 16:40:41 +08:00
342087cd01 fact: Supported Sftp
支持 Sftp 客户端.
2021-08-20 15:25:07 +08:00
3d249696a7 refactor: 修改脚本内组件的传递方式.
将传递脚本组件的方式由 Delegate 类属性改为 Binding, 以增加灵活性和可维护性.
2021-08-20 14:01:43 +08:00
0dc44864cd fix: 调整 Json 字段获取方式以修复由于可选字段不存在导致加载失败的问题.
当 keyPassword 为 null 时, 由于类型检查漏洞, 会出现解析失败的问题.
2021-08-20 14:00:40 +08:00
6bd28909ae refactor: 调整 Trigger 接收闭包的类型, 以便添加新的功能.
调整 Trigger 接收闭包的类型(Runnable -> Closure), 闭包可传递参数, 且 Trigger 为 Groovy 特有(至少目前是这样), 故调整类型以提供更多灵活性.
2021-08-20 13:59:06 +08:00
cbb1784f5e build: 添加 Jacoco 插件, 以分析测试覆盖率.
以后有用, 目前先配置好先.
2021-08-20 02:35:15 +08:00
616179c00a test: 添加两个完整的单元测试.
本次提交添加(补充) GroovyTriggerProvider 和 SshAuthInfoSerializer 的完整单元测试类.
2021-08-20 02:30:56 +08:00
4609e146d9 refactor: 调整编码公钥失败时的动作.
编码公钥失败时的异常不应该被隐藏, 但为了保持鲁棒性, 我决定把异常直接打印到日志中.
2021-08-20 02:29:54 +08:00
35c45a858c refactor: 调整代码以更好的进行测试.
部分代码对测试不友好, 故在不影响原设计的情况下进行了代码调整, 以便更好的编写测试项.
2021-08-20 02:19:11 +08:00
7ac17c6ed7 fix: 修复 Ssh AuthInfo 未加载 keyPassword 的问题.
该问题原因是忘记写了.
2021-08-20 00:46:38 +08:00
8bd55ca2e4 refactor: 添加对参数的非空检查.
添加非空检查以在执行操作前将其拦截.
2021-08-20 00:33:12 +08:00
e54a9513df style: 调整代码, 以防止造成误解.
调整 static 属性与 instance 属性的位置, 以防被视为纳入 instance 属性的一部分.
2021-08-19 23:51:21 +08:00
3d8167b3b4 fact: 添加对 Http Header 的取值.
这个属于是忘记加了的, 原本就计划要弄.
2021-08-19 23:45:54 +08:00
72685ef896 docs: 完善文档. 2021-08-19 23:45:15 +08:00
83161f81fb style: 移除未使用的导入代码. 2021-08-19 23:44:53 +08:00
6b458020ef docs: 完善类文档. 2021-08-19 23:43:18 +08:00
389f23d6d5 style: 移除无用导入和空行. 2021-08-19 23:43:03 +08:00
f48cdaabe9 build: 添加对 Javadoc 的配置.
其实是设置了编码而已.
2021-08-19 19:31:30 +08:00
79d18685e5 refactor: 调整类访问权, 添加文档.
调整以缩小类构造方法访问权.
2021-08-19 19:23:34 +08:00
6d5aea82a4 refactor: 调整 getInstanceState 方法的返回值.
目前遇到脚本无法直接访问 LifecycleState enum 类的情况, 所以暂时调整为返回 String.
2021-08-19 19:22:44 +08:00
39c3695df4 docs: 补充方法文档. 2021-08-19 19:19:47 +08:00
4dd6a9b695 refactor: 包装 Image 对象.
包装 Image 对象以简化脚本对 Image 的访问.
2021-08-19 19:19:11 +08:00
499c3d283c refactor: 设置自动关闭钩子.
设置自动关闭钩子以关闭自动保存线程池.
2021-08-19 19:14:57 +08:00
d5d25dfa42 refactor: 适配更改(Git Commit: bf29faa9).
适配类名更改.
2021-08-19 19:11:38 +08:00
c92c491bd8 docs: 补充文档. 2021-08-19 19:10:12 +08:00
882eabbc71 refactor: 调整类访问权.
减小类可访问范围.
2021-08-19 19:06:22 +08:00
039a020621 refactor: 适配更改(Git Commit: bf29faa9).
适配类名更改.
2021-08-19 19:05:47 +08:00
be905b2976 refactor: 调整构造方法访问权.
最小化开放权限.
2021-08-19 19:04:28 +08:00
bf29faa9a5 refactor: 调整类名, 以更符合其意义.
更改类名和相关属性名.
2021-08-19 18:43:05 +08:00
c47ab110bc docs: 适配更改(Git Commit: 2f97b56d).
文档根据相关修改而进行调整.
2021-08-19 18:37:11 +08:00
2f97b56de1 refactor: 调整 ScriptInfo 中的属性名(artifact -> name).
调整属性名以更适配具体意义.

BREAKING CHANGE: 该变更将影响旧版脚本的初始化过程.
更新方法:
将脚本对 ScriptInfo 的访问按如下进行更改.
旧版:
info {
    artifact 'simple-script'
    group 'org.example'
    version '1.0.0'
}

新版:
info {
    name 'simple-script'
    group 'org.example'
    version '1.0.0'
}

将 artifact 更改为 name 即可.
2021-08-19 18:35:35 +08:00
608e3195ed feat: 增加应用配置文件的初始化功能.
如果不初始化配置文件, 将导致用户修改配置困难.
2021-08-19 18:22:09 +08:00
332bc7a6a1 fix: 修复实例管理器加载已终止实例的问题.
已终止实例已无管理必要, 可以排除.
2021-08-19 17:59:07 +08:00
9905f6ce01 fix: 修复因 Oracle 身份配置内容缺失导致加载失败的问题.
准确来讲该问题并不是很严重, 但还是为此添加了验证步骤以更优雅的告知用户该配置有问题.
2021-08-17 23:30:59 +08:00
541115b9c7 docs: 修复图片引用错误的问题x2. 2021-08-16 13:07:15 +08:00
fa1ba94790 docs: 修复图片引用错误的问题, 补充脚本内容. 2021-08-16 13:05:52 +08:00
65 changed files with 1527 additions and 131 deletions

View File

@ -2,10 +2,11 @@ plugins {
id 'java'
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'jacoco'
}
group 'net.lamgc.oracle'
version '0.0.1-alpha-SNAPSHOT'
version '0.1.0'
compileJava.sourceCompatibility = JavaVersion.VERSION_16
repositories {
@ -43,4 +44,25 @@ dependencies {
test {
useJUnitPlatform()
jacoco {
classDumpDir = file("$buildDir/jacoco/classpathDumps")
}
}
javadoc {
options.encoding = 'UTF-8'
}
jacoco {
reportsDir file("$buildDir/jacoco/reports")
}
jacocoTestReport {
reports {
xml.enabled true
xml.destination file("$buildDir/jacoco/reports.xml")
html.enabled true
html.destination file("$buildDir/jacoco/reports-html/")
}
}

View File

@ -2,3 +2,6 @@
- [安装并使用](安装并使用.md)
- [编写脚本](./script/groovy/入门.md)
- [注册并使用触发器](./script/groovy/注册并使用触发器.md)
- [可用的触发器](./script/groovy/可用的触发器.md)
- [开始使用 SSH](./script/groovy/开始使用SSH.md)

View File

@ -1,12 +1,12 @@
## Groovy 脚本编写
### 基本格式
Groovy 语言的脚本基本格式如下:
Groovy 语言的脚本基本格式如下
```groovy
info {
// 脚本信息, 遵循 Java 的 GAV 规则.
// 脚本英文名
artifact 'demo'
name 'demo'
// 所属组(如果有域名, 就是自己的域名的倒序, 例如 tieba.baidu.com 就是 com.baidu.tieba)
// 如果没有, 也可以用 github 的.
group 'org.example'
@ -34,6 +34,7 @@ def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
// 这里注册定时器触发器.
trigger("timer") {
// time 参数指定 Cron 表达式, 像这样设置表达式字符串后就可以了.
// 比如这里设定一分钟执行一次.
time "0 0/1 * * * ? "
run {
@ -45,3 +46,10 @@ trigger("timer") {
```
Groovy 语言比较贴近 Java实际开发与 Java 没什么区别(相比于 Java 多了不少语法糖,但还是兼容 Java 的)。
## 我能在脚本中使用什么?
目前你能使用的东西有:
- `HTTP`:一个 HttpClient允许你访问 Http 资源。
- `InstanceManager`:计算实例(就是服务器)管理器,可以获取所有的服务器实例,通过实例对象能获取和操作服务器。
除此之外,还有 Java 基本库与 Groovy 基本库可供脚本使用。

View File

@ -0,0 +1,36 @@
## 可用的触发器
### Once 触发器
Once 触发器只会在启动后执行一次,适用于初始化等操作。
示例:
```groovy
trigger ("once") {
run {
// do something...
}
}
```
参数:
- (没有参数)
`it` 对象:无
### Timer 触发器
Timer 触发器通过设定的 Cron 时间表达式,根据时间触发执行,可用于定时检查等操作。
示例:
```groovy
trigger ("once") {
time "0 0 12 * * ? *"
run {
// do something...
}
}
```
参数:
- `time`Cron 时间表达式
`it` 对象:无

View File

@ -0,0 +1,54 @@
## 开始使用 SSH
哨兵为脚本包装了一个 SSH 客户端,通过 SSH 客户端,脚本可以通过 SSH 连接实例并执行命令、通过 Sftp 访问文件、设置端口转发。
### 创建一个 SSH 会话
SSH 会话不止一个,一个脚本可以创建多个 SSH 会话来同时做不同的事情,也可以多个脚本创建多个会话来做不同事情,会话之间互不干扰。
创建会话的方法很简单,只需要这么做:
```groovy
def session = instance.ssh().createSession()
```
这样就能创建一个会话了,至于 SSH 的连接认证什么的,只需要交给哨兵完成即可!
> 注意:会话不保证创建成功,如果创建失败,方法将抛出一个异常。
### 通过 SSH 执行命令
在得到 SSH 会话后,就可以开始执行命令了。
首先需要创建一个命令执行会话(虽然本质上是一个通道):
```groovy
def execSession = session.createExecSession("date")
```
设定后,我们还需要设置命令的标准输出和标准输入,以方便我们获得命令的输出,和向命令输入内容(比如参数):
```groovy
// 这里如果不需要获取并处理的话,可以不设置,
// 也可以将输出设定为哨兵的标准输出,也是可以的。
execSession.setOut(System.out)
// 这里也设置为哨兵的标准输入,可以由管理员主动输入内容。
execSession.setIn(System.in)
```
最后,调用 `exec()` 方法,执行命令并等待命令运行完成即可。
如果需要执行命令后自动输入之类的异步操作呢?可以改用 `exec(true)` 进行异步执行,然后使用 `waitFor()` 等待命令执行完成即可。
命令执行完成后,除了可以检查输出内容来检查程序执行结果外,还可以通过退出代码了解,只需要调用 `exitCode()` 获取退出码即可:
```groovy
if (execSession.exitCode() == 0) {
println "命令执行成功!"
} else {
println "命令执行失败,退出代码不为 0退出码${execSession.exitCode()}"
}
```
### 完整示例代码
```groovy
run {
def session = instance.ssh().createSession()
def execSession = session.createExecSession("date")
execSession.setOut(System.out)
execSession.setIn(System.in)
// 同步执行,并等待命令执行结束。
execSession.exec()
// 除了上述的 if 判断外,也可以使用 Groovy 的字符串嵌入语法。
println "命令执行完成,退出代码:${execSession.exitCode()}"
}
```

View File

@ -0,0 +1,23 @@
## 注册并使用触发器
触发器可以在适当的时候执行脚本所注册的运行代码,使用触发器,哨兵可以在某项事件发生时,触发脚本执行某些操作,而无需脚本手动检查。
### 注册触发器
注册触发器的方法非常简单:
```groovy
trigger("timer") {
// timer 触发器的参数 "time", 填写 Cron 时间表达式.
time "0 0 12 * * ? *"
run {
// 这里编写当到达时间时所需执行的动作.
}
}
```
其中,`"timer"`是触发器名称,`time` 是触发器参数,不同的触发器有不同的参数,也可能没有参数(比如 Once 触发器),`run` 代码块是每个触发器必须有的当满足条件时run 代码块将会被执行。
run 代码块有个隐藏参数 `it`,如果触发器有参数,将通过该参数传递至 run 代码块,供脚本使用。
> 注意:每个触发器所能提供的东西并不一样,具体信息见[触发器文档](可用的触发器.md)。
当注册好了触发器后,只需要等待触发器,在合适的时机触发执行任务即可!

View File

@ -3,22 +3,22 @@
### 创建 API 密钥
前往 [Oracle Cloud](https://cloud.oracle.com),登录后左上角打开菜单,选择“身份和安全”组,在右侧找到“身份”,然后找到“用户”。
![从菜单中找到用户](.\images\Find-the-user-from-the-menu.png)
![从菜单中找到用户](images/Find-the-user-from-the-menu.png)
然后找到自己的账号(一般用注册邮箱命名),如果先前有创建过帐号,找不到自己的帐号,可以在“用户类型”选择“本地”,会方便查找,找到自己的帐号后点进去。
![找到并选择主用户](.\images\Locate-and-select-the-primary-user.png)
![找到并选择主用户](images/Locate-and-select-the-primary-user.png)
进去后左下角资源选择“API密钥”右侧列表选添加 API 密钥,在弹出的窗口中点击“下载私有密钥”,将密钥下载下来妥善保管好(可以先根据需要命名),然后点击“添加”。
![创建 API 机密密钥](.\images\Create-api-secret-key.png)
![创建 API 机密密钥](images/Create-api-secret-key.png)
点击后,会显示一个身份配置模板,将模板复制下来,粘贴到一个文件上,将文件命名为`<自定义名字>.oracle.ini`
![复制身份认证配置](.\images\Create-authentication-profile.png)
![复制身份认证配置](images/Create-authentication-profile.png)
粘贴到文件之后,将刚刚保存好的密钥路径粘贴到`key_file`项里,如图所示:
![粘贴到文件中并设置密钥路径](.\images\Save-to-file.png)
![粘贴到文件中并设置密钥路径](images/Save-to-file.png)
哨兵支持解析密钥*相对于*配置文件的路径,所以可以填相对路径,方便移动配置文件和密钥文件。

View File

@ -1,7 +1,8 @@
package net.lamgc.oracle.sentry;
import com.google.common.base.Throwables;
import com.oracle.bmc.auth.AuthenticationDetailsProvider;
import net.lamgc.oracle.sentry.script.ScriptComponent;
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;
@ -9,19 +10,23 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
/**
* @author LamGC
*/
@Configuration
public class ApplicationInitiation {
class ApplicationInitiation {
private final static Logger log = LoggerFactory.getLogger(ApplicationInitiation.class);
@ -88,7 +93,7 @@ public class ApplicationInitiation {
@Bean("sentry.script.manager")
@Autowired
public ScriptManager initialScriptManager(ComputeInstanceManager instanceManager) {
ScriptComponent context = new ScriptComponent(new ScriptHttpClient(HttpClientBuilder.create()
ScriptComponents context = new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create()
.build()),
instanceManager);
@ -98,6 +103,7 @@ public class ApplicationInitiation {
}
@PostConstruct
@Order(1)
private void initialEnvironment() throws IOException {
String[] directors = new String[] {
"./config",
@ -130,4 +136,28 @@ public class ApplicationInitiation {
}
log.debug("目录检查完成.");
}
@Autowired
private void initialConfigurationFile(ApplicationContext context) {
File configFile = new File("config/application.yml");
if (!configFile.exists()) {
Resource resource = context.getResource("application.yml");
if (!resource.exists()) {
log.error("默认配置初始化失败(包内资源不存在).");
return;
}
try {
Files.copy(resource.getInputStream(), configFile.toPath());
log.info("默认配置文件已初始化完成, 如果调整配置, 可修改配置文件中的相应配置项.");
} catch (IOException e) {
log.error("初始化默认配置文件失败!(Path: {})\n{}",
configFile.getAbsolutePath(), Throwables.getStackTraceAsString(e));
}
} else {
log.debug("配置文件存在, 无需初始化.");
}
}
}

View File

@ -19,6 +19,10 @@ public class ApplicationMain {
@SuppressWarnings("AlibabaConstantFieldShouldBeUpperCase")
private final static Object mainThreadWaiter = new Object();
/**
* 程序入口.
* @param args 程序参数.
*/
public static void main(String[] args) {
SpringApplication.run(ApplicationMain.class, args);

View File

@ -37,6 +37,10 @@ public class ComputeInstanceManager {
sshIdentityProvider.loadAuthInfo();
}
/**
* 获取实例 SSH 认证配置提供器.
* @return 返回 SSH 认证配置提供器.
*/
public SshAuthIdentityProvider getSshIdentityProvider() {
return sshIdentityProvider;
}
@ -86,6 +90,9 @@ public class ComputeInstanceManager {
.compartmentId(compartmentId)
.build());
for (Instance instance : listInstances.getItems()) {
if (instance.getLifecycleState() == Instance.LifecycleState.Terminated) {
continue;
}
ComputeInstance computeInstance = new ComputeInstance(this, instance.getId(),
provider.getUserId(), compartmentId, instance.getImageId(), provider);

View File

@ -10,6 +10,9 @@ import org.springframework.stereotype.Component;
@Component("sentry.constants")
public final class Constants {
/**
* 本类唯一实例, 请不要进行设置.
*/
public static Constants instance;
private Constants() {
@ -21,6 +24,10 @@ public final class Constants {
private String firstConnectionPolicy;
/**
* 获取 SSH 首次连接策略.
* @return 返回策略值.
*/
@NonNull
public String getFirstConnectionPolicy() {
return firstConnectionPolicy;

View File

@ -1,5 +1,6 @@
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;
@ -69,6 +70,9 @@ public final class OracleIdentityManager {
for (File configFile : configFiles) {
try {
AuthenticationDetailsProvider provider = loadFromConfigFile(configFile);
if (provider == null) {
continue;
}
loadedCount ++;
log.info("已成功加载身份配置文件.\n\tUserId: {}\n\tUsername: {}\n\tPath: {}",
provider.getUserId(),
@ -83,7 +87,9 @@ public final class OracleIdentityManager {
/**
* 通过配置文件加载身份信息.
* <p> 加载成功后, 将会注册到身份管理器中.
* @param identityConfig 身份信息文件.
* @return 返回已成功加载后, 配置文件对应的身份配置提供器.
* @throws IOException 如果读取文件发生问题时将抛出该异常.
*/
public AuthenticationDetailsProvider loadFromConfigFile(File identityConfig) throws IOException {
@ -93,6 +99,11 @@ public final class OracleIdentityManager {
ConfigFileReader.ConfigFile config
= ConfigFileReader.parse(identityConfig.getAbsolutePath());
if (!checkIdentityProfileConfig(config)) {
log.warn("该配置文件缺少必要信息, 跳过加载.(Path: {})", identityConfig.getCanonicalPath());
return null;
}
String keyFilePath = config.get("key_file");
if (keyFilePath.startsWith(".")) {
@ -117,6 +128,23 @@ public final class OracleIdentityManager {
return provider;
}
private boolean checkIdentityProfileConfig(ConfigFileReader.ConfigFile config) {
String[] fields = new String[] {
"key_file",
"region",
"tenancy",
"user",
"fingerprint"
};
for (String field : fields) {
if (Strings.isNullOrEmpty(config.get(field))) {
return false;
}
}
return true;
}
/**
* 获取身份所属用户的名称.
* @param provider 身份提供器.

View File

@ -4,10 +4,20 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 输入流包装器.
* <p> 准确来说只是屏蔽了 {@link InputStream#close()} 而已,
* 尝试修复 SSH 命令执行会话可能会关闭设置的输入流的问题.
* @author LamGC
*/
public class InputStreamWrapper extends InputStream {
private final InputStream source;
/**
* 包装一个输入流.
* @param source 输入源.
*/
public InputStreamWrapper(InputStream source) {
this.source = source;
}

View File

@ -3,10 +3,20 @@ package net.lamgc.oracle.sentry.common;
import java.io.IOException;
import java.io.OutputStream;
/**
* 输出流包装器.
* <p> 准确来说只是屏蔽了 {@link OutputStream#close()} 而已,
* 尝试修复 SSH 命令执行会话可能会关闭设置的输出流的问题.
* @author LamGC
*/
public class OutputStreamWrapper extends OutputStream {
private final OutputStream target;
/**
* 包装一个输出流.
* @param target 目标输出流.
*/
public OutputStreamWrapper(OutputStream target) {
this.target = target;
}

View File

@ -0,0 +1,90 @@
package net.lamgc.oracle.sentry.oci.compute;
import com.oracle.bmc.core.model.Image;
import java.util.Date;
/**
* 引导镜像.
* <p> 创建实例时所指定的引导镜像.
* <p> 如果实例经过其他方式重新安装了系统, 则本信息有偏差.
* @author LamGC
*/
public final class BootImage {
private final Image image;
BootImage(Image image) {
this.image = image;
}
/**
* 获取镜像 Id.
* <p> 该 Id 可在创建服务器时指定所使用的系统镜像.
* @return 返回镜像在 Oracle 的 Id.
*/
public String getImageId() {
return image.getId();
}
/**
* 获取镜像所在的区域 Id.
* @return 返回镜像所在区域的 Id.
*/
public String getCompartmentId() {
return image.getCompartmentId();
}
/**
* 获取镜像系统名称.
* <p> 比如 Ubuntu 或者说 CentOS.
* @return 返回系统名称(不是计算机名称).
*/
public String getOS() {
return image.getOperatingSystem();
}
/**
* 获取该镜像基于某一镜像的 Id.
* <p> Oracle 提供了方法, 可以通过当前服务器生成新的镜像,
* 生成后, 新的镜像就是基于原镜像生成, 该项就不为空.
* @return 如果存在, 返回基础镜像 Id, 无基础镜像则返回 {@code null}.
*/
public String getBaseImageId() {
return image.getBaseImageId();
}
/**
* 镜像的显示名称.
* <p> 获取镜像显示名, 该名称与该镜像系统在官方为 iso 的命名差不多.
* @return 获取镜像的显示名称.
*/
public String getName() {
return image.getDisplayName();
}
/**
* 获取镜像大小.
* @return 返回镜像大小, 单位为 MiB.
*/
public Long getSize() {
return image.getSizeInMBs();
}
/**
* 获取系统版本号.
* @return 返回镜像内系统的版本号, 如果版本较旧且服务器更新过系统, 则版本号不是最新的.
*/
public String getOSVersion() {
return image.getOperatingSystemVersion();
}
/**
* 获取镜像创建时间.
* @return 获取镜像创建时间.
*/
public Date getTimeCreated() {
return image.getTimeCreated();
}
}

View File

@ -2,7 +2,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.Image;
import com.oracle.bmc.core.model.Instance;
import com.oracle.bmc.core.requests.GetImageRequest;
import com.oracle.bmc.core.requests.GetInstanceRequest;
@ -33,6 +32,15 @@ public final class ComputeInstance {
private final ComputeClient computeClient;
/**
* 构造一个计算实例对象.
* @param instanceManager 实例所属的管理器.
* @param instanceId 实例 Id.
* @param userId 所属用户 Id.
* @param compartmentId 实例所在区域的 Id.
* @param imageId 镜像 Id.
* @param provider 所属用户的身份配置提供器.
*/
public ComputeInstance(ComputeInstanceManager instanceManager, String instanceId, String userId,
String compartmentId, String imageId, AuthenticationDetailsProvider provider) {
this.instanceManager = instanceManager;
@ -46,33 +54,43 @@ public final class ComputeInstance {
this.network = new InstanceNetwork(this);
}
/**
* 获取实例 Id.
* <p> 可通过实例 Id 直接调用 Oracle Cloud SDK, 也可以作为服务器的唯一标识.
* @return 返回实例 Id.
*/
public String getInstanceId() {
return instanceId;
}
/**
* 获取所属用户的 Id.
* @return 返回用户 Id.
*/
public String getUserId() {
return userId;
}
/**
* 获取服务器所属区域的 Id.
* <p> 使用的资源必须要处于同一区域, 例如 IP 资源, 磁盘.
* @return 返回服务器所属区域的 Id.
*/
public String getCompartmentId() {
return compartmentId;
}
public String getImageId() {
return imageId;
}
/**
* 获取并返回实例镜像信息.
* <p> 可获取系统信息.
* <p> 如果实例被 dd, 则本信息不准确.
* @return 返回实例信息.
*/
public Image getImage() {
public BootImage getImage() {
GetImageResponse image = computeClient.getImage(GetImageRequest.builder()
.imageId(imageId)
.build());
return image.getImage();
return new BootImage(image.getImage());
}
/**
@ -84,37 +102,58 @@ public final class ComputeInstance {
return network;
}
/**
* 获取实例的 SSH 客户端.
* @return 返回实例 SSH 客户端.
*/
public InstanceSsh ssh() {
Instance.LifecycleState instanceState = getInstanceState();
if (instanceState != Instance.LifecycleState.Running) {
String instanceState = getInstanceState();
if (!Instance.LifecycleState.Running.name().equals(instanceState)) {
throw new IllegalStateException("The state of the current instance cannot connect to SSH: " + instanceState);
}
return new InstanceSsh(this, getSshIdentity());
}
public Instance.LifecycleState getInstanceState() {
/**
* 获取实例当前状态.
* <p> 实例可有以下状态:
* <ul>
* <li> Moving: 实例正在转移中;
* <li> Provisioning: 实例正在预分配中(正在创建实例);
* <li> Running: 实例正在运行中;
* <li> Starting: 实例正在启动中;
* <li> Stopping: 实例正在停止中;
* <li> Stopped: 实例已停止运行;
* <li> CreatingImage: 正在通过实例构建镜像;
* <li> Terminating: 正在终止实例(正在删除实例);
* <li> Terminated: 实例已经终止(已删除实例)
* </ul>
* @return 返回实例状态.
*/
public String getInstanceState() {
GetInstanceResponse instance = computeClient.getInstance(GetInstanceRequest.builder()
.instanceId(instanceId)
.build());
return instance.getInstance().getLifecycleState();
return instance.getInstance().getLifecycleState().name();
}
/**
* 对实例执行操作.
* @param action 操作类型.
* @return 如果成功, 返回实例最新状态.
* @return 如果成功, 返回实例最新状态(返回值意义见 {@link #getInstanceState()} 文档).
*/
public Instance.LifecycleState execAction(InstanceAction action) {
public String execAction(InstanceAction action) {
InstanceActionResponse actionResponse = computeClient.instanceAction(InstanceActionRequest.builder()
.instanceId(instanceId)
.action(action.getActionValue())
.build());
return actionResponse.getInstance().getLifecycleState();
return actionResponse.getInstance().getLifecycleState().name();
}
/**
* 获取实例名称.
* @return 返回实例显示名.
*/
public String getInstanceName() {
GetInstanceResponse instance = computeClient.getInstance(GetInstanceRequest.builder()
@ -154,6 +193,8 @@ public final class ComputeInstance {
/**
* 获取 SSH 认证信息.
* @return 返回实例 SSH 认证信息.
* @throws java.util.NoSuchElementException 如果没有指定配置信息则抛出该异常.
*/
private SshAuthInfo getSshIdentity() {
return instanceManager.getSshIdentityProvider()

View File

@ -1,5 +1,10 @@
package net.lamgc.oracle.sentry.oci.compute;
/**
* 实例动作.
* <p> 可对实例执行的操作.
* @author LamGC
*/
public enum InstanceAction {
/**
* 启动实例.
@ -30,6 +35,10 @@ public enum InstanceAction {
this.actionValue = actionValue;
}
/**
* 获取动作的 API 调用值.
* @return 返回 API 所规定的对应值.
*/
public String getActionValue() {
return actionValue;
}

View File

@ -19,7 +19,7 @@ public final class CommandExecSession implements Closeable {
private final ChannelExec channelExec;
public CommandExecSession(ChannelExec channelExec) {
CommandExecSession(ChannelExec channelExec) {
this.channelExec = channelExec;
}
@ -71,6 +71,7 @@ public final class CommandExecSession implements Closeable {
/**
* 设置输入流.
* <p> 设置待执行命令的输入流.
* @param in 待设置的输入流。
*/
public void setIn(InputStream in) {
channelExec.setIn(new InputStreamWrapper(in));
@ -79,6 +80,7 @@ public final class CommandExecSession implements Closeable {
/**
* 设置标准输出流.
* <p> 对应待执行命令的 Stdout.
* @param out 设置标准输出的输出流.
*/
public void setOut(OutputStream out) {
channelExec.setOut(new OutputStreamWrapper(out));
@ -87,6 +89,7 @@ public final class CommandExecSession implements Closeable {
/**
* 设置错误输出流.
* <p> 如果命令使用到, 错误信息会从该输出流输出.
* @param err 设置错误输出的输出流.
*/
public void setErr(OutputStream err) {
channelExec.setErr(new OutputStreamWrapper(err));

View File

@ -16,6 +16,12 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 实例 SSH 客户端.
* <p> 包装并简化了 SSH 会话的创建流程.
* @author LamGC
*/
@SuppressWarnings("unused")
public class InstanceSsh implements AutoCloseable {
private final static Logger log = LoggerFactory.getLogger(InstanceSsh.class);
@ -24,6 +30,11 @@ public class InstanceSsh implements AutoCloseable {
private final SshAuthInfo authInfo;
private final SshClient sshClient;
/**
* 创建连接实例用的 SSH 客户端.
* @param instance SSH 客户端对应的计算实例.
* @param authInfo SSH 认证配置.
*/
public InstanceSsh(ComputeInstance instance, SshAuthInfo authInfo) {
this.instance = Objects.requireNonNull(instance);
this.authInfo = Objects.requireNonNull(authInfo);
@ -43,12 +54,19 @@ public class InstanceSsh implements AutoCloseable {
sshClient.start();
}
/**
* 创建 SSH 会话.
* <p> 允许创建多个 SSH 会话.
* @return 返回新的 SSH 会话.
* @throws IOException 会话创建失败时将抛出异常.
*/
public SshSession createSession() throws IOException {
Set<String> instancePublicIps = instance.network().getInstancePublicIp();
if (instancePublicIps.stream().findFirst().isEmpty()) {
throw new IllegalStateException("Instance has no public IP available.");
}
String connectUri = "ssh://" + authInfo.getUsername() + "@" + instancePublicIps.stream().findFirst().get() + ":22";
String connectUri = "ssh://" + authInfo.getUsername() + "@" +
instancePublicIps.stream().findFirst().get() + ":" + authInfo.getPort();
log.info("SSH 正在连接: {}", connectUri);
ConnectFuture connect = sshClient.connect(connectUri);
connect.verify();
@ -75,7 +93,6 @@ public class InstanceSsh implements AutoCloseable {
}
}
@Override
public void close() {
sshClient.stop();

View File

@ -23,7 +23,7 @@ public class OracleInstanceServerKeyVerifier implements ServerKeyVerifier {
private final ComputeInstance instance;
private final SshAuthInfo info;
public OracleInstanceServerKeyVerifier(ComputeInstance instance, SshAuthInfo info) {
OracleInstanceServerKeyVerifier(ComputeInstance instance, SshAuthInfo info) {
this.instance = instance;
this.info = info;
}

View File

@ -12,10 +12,18 @@ public class PasswordAuthInfo extends SshAuthInfo {
return AuthType.PASSWORD;
}
/**
* 获取 SSH 登录密码.
* @return 返回登录密码.
*/
public String getPassword() {
return password;
}
/**
* 设置 SSH 登录密码.
* @param password 新的登录密码.
*/
public void setPassword(String password) {
this.password = password;
}

View File

@ -1,10 +1,11 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import java.io.File;
import java.security.KeyPair;
/**
* 公钥登录认证配置.
* @author LamGC
*/
public class PublicKeyAuthInfo extends SshAuthInfo{
private File privateKeyPath;
@ -15,18 +16,36 @@ public class PublicKeyAuthInfo extends SshAuthInfo{
return AuthType.PUBLIC_KEY;
}
/**
* 获取私钥路径.
* <p> 注意: 该路径由 SSH 认证配置文件提供, 不保证私钥的存在.
* @return 返回私钥所在路径.
*/
public File getPrivateKeyPath() {
return privateKeyPath;
}
/**
* 设置私钥路径.
* @param privateKeyPath 私钥路径.
*/
public void setPrivateKeyPath(File privateKeyPath) {
this.privateKeyPath = privateKeyPath;
}
/**
* 获取私钥密码.
* @return 如果有, 返回非 {@code null} 值.
*/
public String getKeyPassword() {
return keyPassword;
}
/**
* 设置私钥密码.
* <p> 如果私钥存在密码但未提供密码, 将无法使用私钥验证会话.
* @param keyPassword 私钥密码.
*/
public void setKeyPassword(String keyPassword) {
this.keyPassword = keyPassword;
}

View File

@ -0,0 +1,250 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
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.file.DirectoryNotEmptyException;
import java.nio.file.NoSuchFileException;
import java.util.HashSet;
import java.util.Set;
/**
* Sftp 会话.
* <p> 可通过会话访问远程服务器的文件.
* @author LamGC
*/
@SuppressWarnings("unused")
public class SftpSession implements Closeable {
/**
* 排除的文件名.
*/
private final static Set<String> EXCLUDED_FILE_NAMES;
static {
EXCLUDED_FILE_NAMES = new HashSet<>();
EXCLUDED_FILE_NAMES.add(".");
EXCLUDED_FILE_NAMES.add("..");
}
private final SftpClient sftpClient;
/**
* 创建 Sftp 会话.
* @param sftpClient Sftp 客户端.
*/
SftpSession(SftpClient sftpClient) {
this.sftpClient = sftpClient;
}
/**
* 获取指定文件夹内的所有文件.
* @param path 文件夹路径.
* @return 返回该目录下所有文件的文件名, 文件名不带路径.
* @throws IOException 执行失败时抛出异常.
*/
public Set<String> listFiles(String path) throws IOException {
SftpClient.CloseableHandle handle = sftpClient.openDir(path);
Set<String> paths = new HashSet<>();
for (SftpClient.DirEntry entry : sftpClient.listDir(handle)) {
if (EXCLUDED_FILE_NAMES.contains(entry.getFilename())) {
continue;
}
paths.add(entry.getFilename());
}
sftpClient.close(handle);
return paths;
}
/**
* 读取指定路径的问题.
* @param path 文件所在路径.
* @return 返回文件输入流.
* @throws FileNotFoundException 当文件不存在时抛出该异常.
* @throws IOException 当操作执行失败时抛出异常.
*/
public InputStream read(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
return sftpClient.read(path);
}
/**
* 写入数据到指定目录.
* @param path 待写入的路径.
* @return 返回数据输出流.
* @throws FileNotFoundException 当文件不存在时抛出该异常.
* @throws IOException 如果操作失败则抛出异常.
*/
public OutputStream write(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
return sftpClient.write(path, SftpClient.OpenMode.Write);
}
/**
* 检查指定路径是否存在.
* @param path 待检查的路径.
* @return 如果存在, 返回 {@code true}, 如果文件不存在, 返回 {@code false}.
* @throws IOException 执行失败时抛出.
*/
public boolean exists(String path) throws IOException {
try {
return getAttributes(path) != null;
} catch (IOException e) {
if (e instanceof SftpException sftpException) {
if (sftpException.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
return false;
}
}
throw e;
}
}
/**
* 是否为一个目录.
* @param path 待检查路径.
* @return 如果是一个目录, 返回 {@code true}.
* @throws FileNotFoundException 当路径不存在时抛出该异常.
* @throws IOException 如果执行失败则抛出异常.
*/
public boolean isDirectory(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
return getAttributes(path).isDirectory();
}
/**
* 是否为一个文件.
* @param path 待检查路径.
* @return 如果是一个文件, 返回 {@code true}.
* @throws FileNotFoundException 当路径不存在时抛出该异常.
* @throws IOException 如果执行失败则抛出异常.
*/
public boolean isFile(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
return getAttributes(path).isRegularFile();
}
/**
* 获取文件大小.
* @param path 待获取的路径.
* @return 返回文件大小, 单位 b.
* @throws NoSuchFileException 当指定路径不是一个文件(或符号链接)时抛出.
* @throws FileNotFoundException 当指定路径不存在时抛出.
* @throws IOException 当操作执行失败时抛出.
*/
public long getFileSize(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
SftpClient.Attributes attributes = getAttributes(path);
if (!attributes.isRegularFile()) {
if (attributes.isSymbolicLink()) {
return getFileSize(sftpClient.readLink(path));
}
throw new NoSuchFileException("Not a file: " + path);
}
return attributes.getSize();
}
/**
* 获取指定路径的属性.
* @param path 待获取属性的路径.
* @return 返回路径所属属性.
* @throws IOException 如果执行失败则抛出异常.
*/
public SftpClient.Attributes getAttributes(String path) throws IOException {
return sftpClient.stat(path);
}
/**
* 创建文件夹.
* @param path 待创建的目录.
* @return 当文件夹已存在时返回 {@code false}, 不存在且创建成功则返回 {@code true}.
* @throws IOException 如果操作执行失败则抛出异常.
*/
public boolean mkdir(String path) throws IOException {
if (exists(path)) {
return false;
}
sftpClient.mkdir(path);
return true;
}
/**
* 创建新文件.
* @param path 待创建的文件路径.
* @return 当文件已存在时返回 {@code false}, 不存在且创建成功则返回 {@code true}.
* @throws IOException 如果操作失败啧抛出异常.
*/
public boolean createNewFile(String path) throws IOException {
if (exists(path)) {
return false;
}
SftpClient.CloseableHandle handle = sftpClient.open(path, SftpClient.OpenMode.Create);
sftpClient.close(handle);
return true;
}
/**
* 删除指定路径.
* <p> 该方法内部做了适配, 可兼容文件与文件夹两种类型.
* @param path 待删除的路径.
* @throws IOException 如果删除失败, 抛出异常.
* @throws FileNotFoundException 当指定路径不存在时抛出该异常.
* @throws DirectoryNotEmptyException 当路径为目录且目录不为空时抛出该异常.
*/
public void delete(String path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path);
}
if (isDirectory(path)) {
if (!listFiles(path).isEmpty()) {
throw new DirectoryNotEmptyException(path);
}
sftpClient.rmdir(path);
} else {
sftpClient.remove(path);
}
}
/**
* 强制删除文件夹.
* <p> 如果删除文件夹, 将会对文件夹执行遍历删除, 文件夹及子文件夹内的所有内容都会被删除.
* @param path 待删除的路径.
* @return 如果目标路径为文件夹且删除成功, 返回 {@code true}, 如果是文件, 则不会执行操作并返回 {@code false}.
* 该行为是防止文件遭到误删.
* @throws IOException 如果操作执行失败, 则抛出异常.
*/
public boolean forceDeleteDir(String path) throws IOException {
if (isDirectory(path)) {
for (String filePath : listFiles(path)) {
String fullFilePath = path + "/" + filePath;
if (isDirectory(fullFilePath)) {
forceDeleteDir(fullFilePath);
} else {
sftpClient.remove(fullFilePath);
}
}
sftpClient.rmdir(path);
return true;
} else {
return false;
}
}
@Override
public void close() throws IOException {
sftpClient.close();
}
}

View File

@ -15,6 +15,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
@ -29,7 +30,7 @@ import java.util.stream.Collectors;
* @author LamGC
*/
@SuppressWarnings("UnstableApiUsage")
public final class SshAuthIdentityProvider {
public final class SshAuthIdentityProvider implements AutoCloseable {
private final static String DEFAULT_AUTH_KEY = "@default";
private final static Logger log = LoggerFactory.getLogger(SshAuthIdentityProvider.class);
@ -51,7 +52,11 @@ public final class SshAuthIdentityProvider {
.build());
private final AtomicBoolean needSave = new AtomicBoolean(false);
/**
* 创建 SSH 认证配置提供器.
* @param instanceManager 所属实例管理器.
* @param identityJson 认证配置文件对象.
*/
public SshAuthIdentityProvider(ComputeInstanceManager instanceManager, File identityJson) {
this.instanceManager = instanceManager;
this.identityJsonFile = identityJson;
@ -67,8 +72,14 @@ public final class SshAuthIdentityProvider {
log.warn("本次 SSH 认证配置保存失败.", e);
}
}, 60, 10, TimeUnit.SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(this::close, "Thread-ProviderAutoSave-Close"));
}
/**
* 添加 SSH 认证配置.
* @param instanceId 配置对应的实例 Id.
* @param authInfo SSH 认证配置对象.
*/
public void addSshAuthIdentity(String instanceId, SshAuthInfo authInfo) {
authInfoMap.put(instanceId, authInfo);
}
@ -157,8 +168,13 @@ public final class SshAuthIdentityProvider {
/**
* 获取所有不存在 SSH 配置的实例 Id.
* @return 返回所有不存在对应 SSH 认证配置的实例 Id.
*/
private Set<String> checkForMissingInstances() {
if (instanceManager == null) {
log.info("实例管理器未设置, 跳过检查.");
return Collections.emptySet();
}
Set<String> instanceIdSet = instanceManager.getComputeInstances().stream()
.map(ComputeInstance::getInstanceId)
.collect(Collectors.toSet());
@ -168,4 +184,8 @@ public final class SshAuthIdentityProvider {
return instanceIdSet;
}
@Override
public void close() {
scheduledExec.shutdown();
}
}

View File

@ -3,7 +3,6 @@ package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.PublicKey;
/**
@ -16,11 +15,8 @@ public abstract class SshAuthInfo {
private final static Logger log = LoggerFactory.getLogger(SshAuthInfo.class);
private String username;
/**
* 使用 Sha256 计算的密钥指纹.
*/
private PublicKey serverKey;
private int port;
private SshAuthIdentityProvider provider;
/**
@ -29,14 +25,28 @@ public abstract class SshAuthInfo {
*/
public abstract AuthType getType();
/**
* 获取 SSH 登录用户名.
* @return 返回 SSH 登录用户名.
*/
public String getUsername() {
return username;
}
/**
* 获取服务器公钥.
* <p> 用于认证服务器身份, 在首次登录成功后设置.
* @return 如果之前认证成功并保存过, 则不为 {@code null}, 否则需要进行首次连接确认.
*/
public PublicKey getServerKey() {
return serverKey;
}
/**
* 设置服务器公钥.
* <p> 如果本对象有关联的 {@link SshAuthIdentityProvider}, 则会通知 Provider 保存 SSH 认证配置文件.
* @param serverKey 服务器公钥.
*/
public void setServerKey(PublicKey serverKey) {
this.serverKey = serverKey;
if (this.provider != null) {
@ -44,14 +54,43 @@ public abstract class SshAuthInfo {
}
}
/**
* 设置 SSH 登录用户名.
* @param username 登录 SSH 的用户名.
*/
public void setUsername(String username) {
this.username = username;
}
/**
* 设置 SSH 连接端口.
* @param port SSH 端口号.
*/
public void setPort(int port) {
this.port = port;
}
/**
* 获取 SSH 端口号.
* @return 返回 SSH 端口号.
*/
public int getPort() {
return port;
}
/**
* 设置 SSH 认证配置提供器.
* <p> 设置后, 可在首次连接认证通过后, 保存服务器公钥到文件中.
* @param provider 所属提供器对象.
*/
void setProvider(SshAuthIdentityProvider provider) {
this.provider = provider;
}
/**
* 认证类型.
* <p> 如果没有所需认证类型, 就是没支持.
*/
public enum AuthType {
/**
* 密码认证.
@ -68,6 +107,10 @@ public abstract class SshAuthInfo {
this.targetClass = targetClass;
}
/**
* 获取类型所属的认证配置类.
* @return 返回认证配置类.
*/
public Class<? extends SshAuthInfo> getTargetClass() {
return targetClass;
}

View File

@ -5,6 +5,8 @@ import com.google.gson.*;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
@ -23,18 +25,21 @@ import java.util.Collections;
*/
public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>, JsonDeserializer<SshAuthInfo> {
private final static Logger log = LoggerFactory.getLogger(SshAuthInfoSerializer.class);
/**
* 本类唯一实例.
* <p> 序列化器支持多用.
*/
public final static SshAuthInfoSerializer INSTANCE = new SshAuthInfoSerializer();
private SshAuthInfoSerializer() {}
@Override
public SshAuthInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject()) {
throw new JsonParseException("It should be a JsonObject");
}
JsonObject infoObject = json.getAsJsonObject();
String type = getFieldToStringOrFail(infoObject, "authType");
SshAuthInfo.AuthType authType = SshAuthInfo.AuthType.valueOf(type.toUpperCase());
SshAuthInfo.AuthType authType = getAuthType(type);
SshAuthInfo info;
if (authType == SshAuthInfo.AuthType.PASSWORD) {
PasswordAuthInfo pswAuthInfo = new PasswordAuthInfo();
@ -45,18 +50,40 @@ public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>,
String privateKeyPath = getFieldToStringOrFail(infoObject, "privateKeyPath");
File privateKeyFile = new File(privateKeyPath);
publicKeyInfo.setPrivateKeyPath(privateKeyFile);
publicKeyInfo.setKeyPassword(getFieldToString(infoObject, "keyPassword"));
info = publicKeyInfo;
} else {
throw new JsonParseException("Unsupported authentication type: " + authType);
}
info.setUsername(getFieldToStringOrFail(infoObject, "username"));
try {
info.setServerKey(decodeSshPublicKey(
infoObject.has("serverKey") && infoObject.get("serverKey").isJsonPrimitive() ?
infoObject.get("serverKey").getAsString() :
null));
} catch (GeneralSecurityException | IOException e) {
throw new JsonParseException(e);
String portStr = getFieldToString(infoObject, "port");
if (portStr != null) {
try {
int port = Integer.parseInt(portStr);
if (checkPortNumber(port)) {
info.setPort(port);
} else {
log.warn("端口号非法, 将使用默认端口号.(Input: {})", port);
info.setPort(22);
}
} catch (NumberFormatException e) {
log.warn("端口号无法转换成数字, 端口号将使用默认端口号.(Input: {})", portStr);
info.setPort(22);
}
} else {
info.setPort(22);
}
String serverKeyStr = getFieldToString(infoObject, "serverKey");
if (!Strings.isNullOrEmpty(serverKeyStr)) {
try {
info.setServerKey(decodeSshPublicKey(serverKeyStr));
} catch (GeneralSecurityException | IOException e) {
info.setServerKey(null);
log.error("解析 ServerKey 时发生错误, 该 ServerKey 将为空.(后续连接需进行首次连接认证.)", e);
}
} else {
info.setServerKey(null);
}
return info;
}
@ -64,13 +91,6 @@ public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>,
@Override
public JsonElement serialize(SshAuthInfo src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
json.addProperty("authType", src.getType().toString());
json.addProperty("username", src.getUsername());
try {
json.addProperty("serverKey", encodeSshPublicKey(src.getServerKey()));
} catch (IOException e) {
throw new JsonParseException(e);
}
if (src instanceof PasswordAuthInfo info) {
json.addProperty("password", info.getPassword());
} else if (src instanceof PublicKeyAuthInfo info) {
@ -83,21 +103,37 @@ public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>,
} else {
throw new JsonParseException("Unsupported type");
}
json.addProperty("authType", src.getType().toString());
json.addProperty("username", src.getUsername());
json.addProperty("port", src.getPort());
if (src.getServerKey() != null) {
json.addProperty("serverKey", encodeSshPublicKey(src.getServerKey()));
} else {
json.add("serverKey", JsonNull.INSTANCE);
}
return json;
}
private boolean checkPortNumber(int port) {
return port >= 0 && port <= 65535;
}
private String getFieldToStringOrFail(JsonObject object, String field) {
if (!object.has(field)) {
if (!object.has(field) || !object.get(field).isJsonPrimitive()) {
throw new JsonParseException("Missing field: " + field);
}
return object.get(field).getAsString();
}
private PublicKey decodeSshPublicKey(String publicKeyString) throws GeneralSecurityException, IOException {
if (Strings.isNullOrEmpty(publicKeyString)) {
private String getFieldToString(JsonObject object, String field) {
if (!object.has(field) || !object.get(field).isJsonPrimitive()) {
return null;
}
return object.get(field).getAsString();
}
private PublicKey decodeSshPublicKey(String publicKeyString) throws GeneralSecurityException, IOException {
String[] strings = publicKeyString.split(" ", 3);
@SuppressWarnings("unchecked") PublicKeyEntryDecoder<PublicKey, ?> decoder =
@ -105,14 +141,23 @@ public final class SshAuthInfoSerializer implements JsonSerializer<SshAuthInfo>,
return decoder.decodePublicKey(null, strings[0], Base64.getDecoder().decode(strings[1]), Collections.emptyMap());
}
private String encodeSshPublicKey(PublicKey key) throws IOException {
if (key == null) {
private String encodeSshPublicKey(PublicKey key) {
try {
StringBuilder builder = new StringBuilder();
PublicKeyEntry.appendPublicKeyEntry(builder, key);
return builder.toString();
} catch (IOException e) {
log.error("ServerKey 编码失败, 下次加载时需要进行首次连接认证.", e);
}
return null;
}
private SshAuthInfo.AuthType getAuthType(String type) {
try {
return SshAuthInfo.AuthType.valueOf(type.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
StringBuilder builder = new StringBuilder();
PublicKeyEntry.appendPublicKeyEntry(builder, key);
return builder.toString();
}
}

View File

@ -1,6 +1,7 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.sftp.client.SftpClientFactory;
import java.io.Closeable;
import java.io.IOException;
@ -15,7 +16,11 @@ public class SshSession implements Closeable {
private final ClientSession clientSession;
public SshSession(ClientSession clientSession) {
/**
* 创建新的 SSH 会话.
* @param clientSession 原始 SSH 会话.
*/
SshSession(ClientSession clientSession) {
this.clientSession = clientSession;
}
@ -29,6 +34,17 @@ public class SshSession implements Closeable {
return new CommandExecSession(clientSession.createExecChannel(command));
}
/**
* 创建 Sftp 会话.
* <p> 可通过会话操作 Sftp.
* @return 返回 Sftp 会话.
* @throws IOException 如果创建失败, 将抛出异常.
*/
public SftpSession createSftpSession() throws IOException {
SftpClientFactory factory = SftpClientFactory.instance();
return new SftpSession(factory.createSftpClient(clientSession));
}
/**
* 关闭 SSH 连接会话, 该连接会话所属的其他会话将会一同被关闭.
* @throws IOException 关闭失败时抛出异常,

View File

@ -4,9 +4,12 @@ import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
/**
* 脚本组件集合.
* <p> 存储了脚本可以使用的对象.
* <p> 后续可能会改成用 {@link javax.script.Bindings} 之类的.
* @author LamGC
*/
public final record ScriptComponent(
public final record ScriptComponents(
ScriptHttpClient HTTP,
ComputeInstanceManager InstanceManager
) {

View File

@ -4,29 +4,42 @@ import java.util.Objects;
/**
* 脚本信息.
* <p> 脚本信息的 Group, Name 和 Version 遵循 Java 依赖管理的 GAV 坐标规则。
* @author LamGC
*/
public class ScriptInfo {
private String group;
private String artifact;
private String name;
private String version;
/**
* 获取组名.
* @return 返回组名.
*/
public String getGroup() {
return group;
}
public String getArtifact() {
return artifact;
/**
* 获取组名.
* @return 返回组名.
*/
public String getName() {
return name;
}
/**
* 获取组名.
* @return 返回组名.
*/
public String getVersion() {
return version;
}
@Override
public String toString() {
return getGroup() + ":" + getArtifact() + ":" + getVersion();
return getGroup() + ":" + getName() + ":" + getVersion();
}
@Override
@ -38,22 +51,34 @@ public class ScriptInfo {
return false;
}
ScriptInfo that = (ScriptInfo) o;
return group.equals(that.group) && artifact.equals(that.artifact) && version.equals(that.version);
return group.equals(that.group) && name.equals(that.name) && version.equals(that.version);
}
@Override
public int hashCode() {
return Objects.hash(group, artifact, version);
return Objects.hash(group, name, version);
}
/**
* 设置组名.
* @param group 新的组名.
*/
public void setGroup(String group) {
this.group = group;
}
public void setArtifact(String artifact) {
this.artifact = artifact;
/**
* 设置脚本名称.
* @param name 设置脚本名称.
*/
public void setName(String name) {
this.name = name;
}
/**
* 设置版本号.
* @param version 脚本版本号.
*/
public void setVersion(String version) {
this.version = version;
}

View File

@ -22,7 +22,7 @@ public interface ScriptLoader {
* @return 返回脚本对象.
* @throws Exception 当 Loader 抛出异常时, 将视为脚本加载失败, 该脚本跳过加载.
*/
Script loadScript(ScriptComponent context, File scriptFile) throws Exception;
Script loadScript(ScriptComponents context, File scriptFile) throws Exception;
/**
* 获取脚本信息.

View File

@ -19,13 +19,18 @@ public final class ScriptManager {
private final Set<ScriptLoader> loaders = new HashSet<>();
private final File scriptsLocation;
private final ScriptComponent context;
private final ScriptComponents context;
private final Map<ScriptInfo, Script> scripts = new ConcurrentHashMap<>();
public ScriptManager(File scriptsLocation, ScriptComponent context) {
/**
* 创建新的脚本管理器.
* @param scriptsLocation 脚本加载路径.
* @param components 脚本组件.
*/
public ScriptManager(File scriptsLocation, ScriptComponents components) {
this.scriptsLocation = scriptsLocation;
this.context = context;
this.context = components;
loadScriptLoaders();
}

View File

@ -16,14 +16,18 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods;
public class GroovyDslDelegate implements Script {
private final GroovyScriptInfo scriptInfo = new GroovyScriptInfo();
private final ScriptHttpClient HTTP;
private final ComputeInstanceManager InstanceManager;
public GroovyDslDelegate(ScriptHttpClient httpClient, ComputeInstanceManager instanceManager) {
HTTP = httpClient;
InstanceManager = instanceManager;
/**
* 构建一个 DSL Delegate, 并传入可操作对象.
*/
public GroovyDslDelegate() {
}
/**
* 注册触发器.
* @param triggerName 触发器名称.
* @param closure 待执行闭包.
*/
private void trigger(String triggerName, Closure<?> closure){
DefaultGroovyMethods.with(GroovyTriggerProvider.INSTANCE.getTriggerByName(triggerName), closure);
}

View File

@ -2,16 +2,38 @@ package net.lamgc.oracle.sentry.script.groovy;
import net.lamgc.oracle.sentry.script.ScriptInfo;
/**
* 适配 Groovy 的脚本信息对象.
* @author LamGC
*/
public class GroovyScriptInfo extends ScriptInfo {
public void artifact(String artifact) {
super.setArtifact(artifact);
/**
* 设置脚本名.
* <p> 不能有空格.
* @param name 脚本名.
*/
public void name(String name) {
super.setName(name);
}
/**
* 设置组名.
* <p> 组名是脚本开发者的域名倒写, 如果你的域名是 example.com,
* 那么组名就是 com.example, 没有域名可以用 Github 的,
* io.github.[你的 Github 用户名]
*
* @param group 组名.
*/
public void group(String group) {
super.setGroup(group);
}
/**
* 脚本版本号.
* <p> 遵循 SemVer 版本号规范.
* @param version 当前脚本版本号.
*/
public void version(String version) {
super.setVersion(version);
}

View File

@ -1,10 +1,11 @@
package net.lamgc.oracle.sentry.script.groovy;
import com.google.common.base.Throwables;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.util.DelegatingScript;
import net.lamgc.oracle.sentry.script.Script;
import net.lamgc.oracle.sentry.script.ScriptComponent;
import net.lamgc.oracle.sentry.script.ScriptComponents;
import net.lamgc.oracle.sentry.script.ScriptInfo;
import net.lamgc.oracle.sentry.script.ScriptLoader;
import org.codehaus.groovy.control.CompilerConfiguration;
@ -14,11 +15,13 @@ 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.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Groovy 脚本加载器.
* @author LamGC
*/
@SuppressWarnings("MapOrSetKeyShouldOverrideHashCodeEquals")
@ -30,6 +33,10 @@ public class GroovyScriptLoader implements ScriptLoader {
private final Map<Script, ScriptInfo> scriptInfoMap = new ConcurrentHashMap<>();
/**
* 构造一个新的脚本加载器.
* <p> 每个加载器所使用的 {@link GroovyClassLoader} 实例是不一样的.
*/
public GroovyScriptLoader() {
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setScriptBaseClass(DelegatingScript.class.getName());
@ -42,7 +49,7 @@ public class GroovyScriptLoader implements ScriptLoader {
}
@Override
public Script loadScript(ScriptComponent context, File scriptFile) throws IOException {
public Script loadScript(ScriptComponents context, File scriptFile) throws IOException {
Class<?> scriptClass = scriptClassLoader.parseClass(scriptFile);
if (!DelegatingScript.class.isAssignableFrom(scriptClass)) {
return null;
@ -51,8 +58,9 @@ public class GroovyScriptLoader implements ScriptLoader {
Constructor<? extends DelegatingScript> constructor =
scriptClass.asSubclass(DelegatingScript.class).getConstructor();
DelegatingScript newScriptObject = constructor.newInstance();
GroovyDslDelegate dslDelegate = new GroovyDslDelegate(context.HTTP(), context.InstanceManager());
GroovyDslDelegate dslDelegate = new GroovyDslDelegate();
newScriptObject.setDelegate(dslDelegate);
newScriptObject.setBinding(createBinding(context));
newScriptObject.run();
scriptInfoMap.put(dslDelegate, dslDelegate.getScriptInfo());
return dslDelegate;
@ -66,4 +74,19 @@ public class GroovyScriptLoader implements ScriptLoader {
public ScriptInfo getScriptInfo(Script script) {
return scriptInfoMap.get(script);
}
private static Binding createBinding(ScriptComponents components) {
Binding binding = new Binding();
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();
}
}
return binding;
}
}

View File

@ -3,23 +3,28 @@ package net.lamgc.oracle.sentry.script.groovy;
import com.google.common.base.Strings;
import net.lamgc.oracle.sentry.script.groovy.trigger.GroovyTrigger;
import net.lamgc.oracle.sentry.script.groovy.trigger.TriggerName;
import org.springframework.scheduling.Trigger;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
* Groovy 脚本语言的触发器提供者.
* <p> 根据脚本需要创建并注册触发器.
* @author LamGC
*/
public class GroovyTriggerProvider {
private final Map<String, ServiceLoader.Provider<GroovyTrigger>> triggerProviderMap = new ConcurrentHashMap<>();
/**
* Trigger Provider 唯一实例.
*/
public final static GroovyTriggerProvider INSTANCE = new GroovyTriggerProvider();
private final Map<String, ServiceLoader.Provider<GroovyTrigger>> triggerProviderMap = new ConcurrentHashMap<>();
private GroovyTriggerProvider() {
ServiceLoader<GroovyTrigger> loader = ServiceLoader.load(GroovyTrigger.class);
loader.stream().iterator().forEachRemaining(triggerProvider -> {
@ -40,7 +45,14 @@ public class GroovyTriggerProvider {
});
}
/**
* 通过 Trigger 名称获取新的 Trigger.
* @param triggerName Trigger 名称.
* @return 返回指定 Trigger 的新实例.
* @throws NoSuchElementException 当指定的 Trigger 名称没有对应 Trigger 时抛出该异常.
*/
public GroovyTrigger getTriggerByName(String triggerName) {
Objects.requireNonNull(triggerName);
if (!triggerProviderMap.containsKey(triggerName.toLowerCase())) {
throw new NoSuchElementException("The specified trigger could not be found: " + triggerName);
}

View File

@ -1,8 +1,11 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import groovy.lang.DelegatesTo;
import groovy.lang.Closure;
/**
* Groovy 脚本的触发器接口.
* <p> 实现该接口并添加 {@link TriggerName} 注解后,
* 添加到 SPI 实现列表, 即可作为一个 Trigger.
* @author LamGC
*/
public interface GroovyTrigger {
@ -12,8 +15,6 @@ public interface GroovyTrigger {
* <p> 注意, 触发器执行 run 方法不可以阻塞方法返回.
* @param task 触发器需要执行的任务.
*/
void run(Runnable task);
void run(Closure<?> task);
}

View File

@ -1,6 +1,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;
@ -23,7 +24,7 @@ public class OnceTrigger implements GroovyTrigger {
.build());
@Override
public void run(Runnable task) {
public void run(Closure<?> task) {
EXECUTOR.execute(task);
}
}

View File

@ -2,6 +2,7 @@ package net.lamgc.oracle.sentry.script.groovy.trigger;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import groovy.lang.Closure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@ -38,7 +39,7 @@ public class TimerTrigger implements GroovyTrigger {
}
@Override
public void run(Runnable runnable) {
public void run(Closure<?> runnable) {
if (trigger == null) {
if (!log.isDebugEnabled()) {
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
@ -57,7 +58,21 @@ public class TimerTrigger implements GroovyTrigger {
return;
}
SCHEDULER.schedule(runnable, trigger);
SCHEDULER.schedule(new TimerTaskRunnable(runnable), trigger);
}
private static class TimerTaskRunnable implements Runnable {
private final Closure<?> closure;
private TimerTaskRunnable(Closure<?> closure) {
this.closure = closure;
}
@Override
public void run() {
closure.call();
}
}
}

View File

@ -13,6 +13,11 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface TriggerName {
/**
* Trigger 名称.
* <p> 需保证唯一性.
* @return 返回 Trigger 名称.
*/
String value();
}

View File

@ -10,7 +10,8 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
*
* Http 访问对象.
* <p> 该对象可以复用.
* @author LamGC
*/
public class HttpAccess {
@ -23,12 +24,23 @@ public class HttpAccess {
this.url = url;
}
/**
* 以 Get 方法发起 Http 请求.
* @return 返回 Http 响应对象.
* @throws IOException 当请求发送失败时抛出异常.
*/
public HttpAccessResponse get() throws IOException {
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
return new HttpAccessResponse(response);
}
/**
* 以 Post 方法发起 Http 请求.
* @param body Post 请求体.
* @return 返回 Http 响应对象.
* @throws IOException 当请求发送失败时抛出异常.
*/
public HttpAccessResponse post(String body) throws IOException {
HttpPost request = new HttpPost(url);
request.setEntity(new StringEntity(body, StandardCharsets.UTF_8));

View File

@ -6,7 +6,6 @@ import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
@ -32,20 +31,45 @@ public final class HttpAccessResponse {
this.entity = response.getEntity();
}
/**
* 获取响应状态行.
* @return 返回响应状态行, 包括响应码和信息.
*/
public StatusLine getStatusLine() {
return statusLine;
}
/**
* 获取语言.
* @return 返回 Locale 对象.
*/
public Locale getLocale() {
return locale;
}
/**
* 将 ResponseBody 转为字符串并返回.
* @return 返回字符串形式的响应体.
* @throws IOException 当接收失败时抛出异常.
*/
public String getContentToString() throws IOException {
return EntityUtils.toString(entity);
}
/**
* 获取响应体实体, 可手动接收 Http Response Body.
* @return 返回 Http 实体.
*/
public HttpEntity getEntity() {
return entity;
}
/**
* 获取 Header.
* @param name Header 名称.
* @return 如果存在, 返回相应值, 否则返回 {@code null}.
*/
public String getHeader(String name) {
return headers.get(name);
}
}

View File

@ -2,10 +2,19 @@ package net.lamgc.oracle.sentry.script.tools.http;
import org.apache.http.client.HttpClient;
/**
* 创建脚本使用的 HttpClient 包装对象.
* <p> 可根据脚本需要优化和简化步骤.
* @author LamGC
*/
public class ScriptHttpClient {
private final HttpClient httpClient;
/**
* 包装并构造一个脚本 Http 客户端.
* @param httpClient 原始 Http 客户端.
*/
public ScriptHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
@ -13,6 +22,7 @@ public class ScriptHttpClient {
/**
* 打开一个连接.
* @param url 要访问的 Url.
* @return 返回 Http 访问对象(可重复使用).
*/
public HttpAccess create(String url) {
return new HttpAccess(httpClient, url);

View File

@ -0,0 +1,20 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import java.security.PublicKey;
public class BadPublicKey implements PublicKey {
@Override
public String getAlgorithm() {
return null;
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return new byte[0];
}
}

View File

@ -3,43 +3,50 @@ package net.lamgc.oracle.sentry.oci.compute.ssh;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
/**
* @see SshAuthInfoSerializer
*/
class SshAuthInfoSerializerTest {
private final static Gson gson = new GsonBuilder()
.registerTypeAdapter(SshAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
.registerTypeAdapter(PasswordAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
.registerTypeAdapter(PublicKeyAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
.registerTypeAdapter(UnsupportedSshAuthInfo.class, SshAuthInfoSerializer.INSTANCE)
.serializeNulls()
.create();
private JsonObject getPasswordAuthObject() {
return gson.fromJson("""
{
"username": "opc",
"authType": "password",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}
""", JsonObject.class);
}
private JsonObject getPublicKeyAuthObject() {
return gson.fromJson("""
{
"username": "opc",
"authType": "Public_Key",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"privateKeyPath": "",
"keyPassword": ""
}
""", JsonObject.class);
private JsonObject getTestsInfo(String name) {
InputStream resource = this.getClass().getResourceAsStream("/ssh-auth/" + name + ".json");
if (resource == null) {
throw new NoSuchElementException("Required resource not found: " + name);
}
return gson.fromJson(new InputStreamReader(resource, StandardCharsets.UTF_8), JsonObject.class);
}
@Test
public void deserializeTest() {
SshAuthInfo info = gson.fromJson(getPasswordAuthObject(), SshAuthInfo.class);
public void deserializePasswordTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("StandardPassword"), SshAuthInfo.class);
assertTrue(info instanceof PasswordAuthInfo);
assertEquals("opc", info.getUsername());
@ -47,4 +54,206 @@ class SshAuthInfoSerializerTest {
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
}
@Test
public void deserializePublicKeyTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("StandardPublicKey"), SshAuthInfo.class);
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
assertEquals("opc", info.getUsername());
if (info instanceof PublicKeyAuthInfo pkInfo) {
assertEquals(new File("~/.ssh/id_rsa"), pkInfo.getPrivateKeyPath());
assertEquals("123456", pkInfo.getKeyPassword());
} else {
fail("The type of the parsing result does not match: " + info.getClass());
}
}
@Test
public void deserializeBadPortNumberTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("BadPortValue-NonNumber"), SshAuthInfo.class);
assertTrue(info instanceof PasswordAuthInfo);
assertEquals("opc", info.getUsername());
assertEquals("123456", ((PasswordAuthInfo) info).getPassword());
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
assertEquals(22, info.getPort());
}
@Test
public void deserializePortNumberOutOfBoundTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("BadPortValue-OutOfBound"), SshAuthInfo.class);
assertTrue(info instanceof PasswordAuthInfo);
assertEquals("opc", info.getUsername());
assertEquals("123456", ((PasswordAuthInfo) info).getPassword());
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
assertEquals(22, info.getPort());
}
@Test
public void deserializePortNumberOutOfBoundMinusTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("BadPortValue-OutOfBound-minus"), SshAuthInfo.class);
assertTrue(info instanceof PasswordAuthInfo);
assertEquals("opc", info.getUsername());
assertEquals("123456", ((PasswordAuthInfo) info).getPassword());
assertEquals("SHA256:qBu2jRXM6Wog/jWUJJ0WLTMb3UdDGAmYEVZQNZdFZNM", KeyUtils.getFingerPrint(info.getServerKey()));
assertEquals(22, info.getPort());
}
@Test
public void deserializeUnsupportedTest() {
assertThrows(JsonParseException.class, () ->
gson.fromJson(getTestsInfo("UnsupportedAuthType"), SshAuthInfo.class));
}
@Test
public void deserializeNoExistTypeTest() {
assertThrows(JsonParseException.class, () ->
gson.fromJson(getTestsInfo("NoExistType"), SshAuthInfo.class));
}
@Test
public void deserializeBadServerKeyFieldTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("BadServerKeyField"), SshAuthInfo.class);
assertNull(info.getServerKey());
}
@Test
public void deserializeBadServerKeyDecodeTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("BadServerKey-decode"), SshAuthInfo.class);
assertNull(info.getServerKey());
}
@Test
public void deserializeNoExistServerKeyTest() {
SshAuthInfo info = gson.fromJson(getTestsInfo("ServerKeyNoExist"), SshAuthInfo.class);
assertNull(info.getServerKey());
}
@Test
public void deserializeUnsupportedJsonTypeTest() {
assertThrows(JsonParseException.class, () ->
gson.fromJson(getTestsInfo("UnsupportedJsonType"), SshAuthInfo.class));
}
@Test
public void deserializeBadRequiredFieldJsonTypeTest() {
assertThrows(JsonParseException.class, () ->
gson.fromJson(getTestsInfo("BadRequiredFieldType"), SshAuthInfo.class));
}
private void initialSshAuthInfo(SshAuthInfo info) {
try {
KeyPair pair = KeyUtils.generateKeyPair("ssh-rsa", 3072);
info.setServerKey(pair.getPublic());
info.setPort(new Random().nextInt(65536));
info.setUsername("linux");
if (info instanceof PasswordAuthInfo psw) {
psw.setPassword(String.valueOf(new Random().nextLong()));
} else if (info instanceof PublicKeyAuthInfo pk) {
pk.setKeyPassword(String.valueOf(new Random().nextLong()));
pk.setPrivateKeyPath(new File("./" + new Random().nextLong() + "/key"));
}
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private String getOrFailField(JsonObject json, String field) {
if (json.has(field) && json.get(field).isJsonPrimitive()) {
return json.get(field).getAsString();
} else {
fail("The JSON field '" + field + "' does not exist or is not a primitive.");
throw new RuntimeException();
}
}
private PublicKey decodeSshPublicKey(String publicKeyString) throws GeneralSecurityException, IOException {
String[] strings = publicKeyString.split(" ", 3);
@SuppressWarnings("unchecked") PublicKeyEntryDecoder<PublicKey, ?> decoder =
(PublicKeyEntryDecoder<PublicKey, ?>) KeyUtils.getPublicKeyEntryDecoder(strings[0]);
return decoder.decodePublicKey(null, strings[0], Base64.getDecoder().decode(strings[1]), Collections.emptyMap());
}
@Test
public void serializePasswordTest() throws GeneralSecurityException, IOException {
PasswordAuthInfo info = new PasswordAuthInfo();
initialSshAuthInfo(info);
JsonObject json = gson.fromJson(gson.toJson(info), JsonObject.class);
assertEquals(SshAuthInfo.AuthType.PASSWORD.name(), getOrFailField(json, "authType"));
assertEquals(KeyUtils.getFingerPrint(info.getServerKey()),
KeyUtils.getFingerPrint(decodeSshPublicKey(getOrFailField(json, "serverKey"))));
assertEquals(info.getPort(), Integer.parseInt(getOrFailField(json, "port")));
assertEquals(info.getUsername(), getOrFailField(json, "username"));
assertEquals(info.getPassword(), getOrFailField(json, "password"));
}
@Test
public void serializePublicKeyTest() throws GeneralSecurityException, IOException {
PublicKeyAuthInfo info = new PublicKeyAuthInfo();
initialSshAuthInfo(info);
JsonObject json = gson.fromJson(gson.toJson(info), JsonObject.class);
assertEquals(SshAuthInfo.AuthType.PUBLIC_KEY.name(), getOrFailField(json, "authType"));
assertEquals(KeyUtils.getFingerPrint(info.getServerKey()),
KeyUtils.getFingerPrint(decodeSshPublicKey(getOrFailField(json, "serverKey"))));
assertEquals(info.getUsername(), getOrFailField(json, "username"));
assertEquals(info.getPort(), Integer.parseInt(getOrFailField(json, "port")));
assertEquals(info.getPrivateKeyPath().getCanonicalFile(), new File(getOrFailField(json, "privateKeyPath")));
assertEquals(info.getKeyPassword(), getOrFailField(json, "keyPassword"));
}
@Test
public void serializeNoExistServerKeyTest() {
PasswordAuthInfo info = new PasswordAuthInfo();
initialSshAuthInfo(info);
info.setServerKey(null);
JsonObject json = gson.fromJson(gson.toJson(info), JsonObject.class);
assertEquals(SshAuthInfo.AuthType.PASSWORD.name(), getOrFailField(json, "authType"));
assertTrue(json.get("serverKey").isJsonNull());
assertEquals(info.getUsername(), getOrFailField(json, "username"));
assertEquals(info.getPort(), Integer.parseInt(getOrFailField(json, "port")));
assertEquals(info.getPassword(), getOrFailField(json, "password"));
}
@Test
public void serializeUnsupportedTest() {
assertThrows(JsonParseException.class, () ->
gson.toJson(new UnsupportedSshAuthInfo(false)));
}
@Test
public void serializeBadPrivateKeyPathTest() {
PublicKeyAuthInfo info = new PublicKeyAuthInfo();
initialSshAuthInfo(info);
info.setPrivateKeyPath(new File("@#$*%&&96137:()*/key"));
assertThrows(JsonParseException.class, () ->
gson.toJson(info));
}
@Test
public void serializeBadServerKeyTest() {
PasswordAuthInfo info = new PasswordAuthInfo();
initialSshAuthInfo(info);
info.setServerKey(new BadPublicKey());
JsonObject json = gson.fromJson(gson.toJson(info), JsonObject.class);
assertEquals(SshAuthInfo.AuthType.PASSWORD.name(), getOrFailField(json, "authType"));
assertTrue(json.get("serverKey").isJsonNull());
assertEquals(info.getUsername(), getOrFailField(json, "username"));
assertEquals(info.getPort(), Integer.parseInt(getOrFailField(json, "port")));
assertEquals(info.getPassword(), getOrFailField(json, "password"));
}
}

View File

@ -0,0 +1,15 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
public class UnsupportedSshAuthInfo extends SshAuthInfo {
private final boolean returnType;
public UnsupportedSshAuthInfo(boolean returnType) {
this.returnType = returnType;
}
@Override
public AuthType getType() {
return returnType ? AuthType.PASSWORD : null;
}
}

View File

@ -12,7 +12,7 @@ class ScriptManagerTest {
@Test
public void loadScriptTest() {
ScriptManager manager = new ScriptManager(new File("./run/scripts"),
new ScriptComponent(new ScriptHttpClient(HttpClientBuilder.create().build()),
new ScriptComponents(new ScriptHttpClient(HttpClientBuilder.create().build()),
new ComputeInstanceManager()));
manager.loadScripts();

View File

@ -0,0 +1,77 @@
package net.lamgc.oracle.sentry.script.groovy;
import net.lamgc.oracle.sentry.script.groovy.trigger.*;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceLoader;
/**
* @see GroovyTriggerProvider
*/
class GroovyTriggerProviderTest {
@Test
public void standardRunTest() {
GroovyTrigger trigger = GroovyTriggerProvider.INSTANCE.getTriggerByName("once");
Assertions.assertNotNull(trigger);
Assertions.assertEquals(OnceTrigger.class, trigger.getClass());
}
@Test
public void noAnnotationTriggerTest() throws NoSuchFieldException, IllegalAccessException {
failIfHasTrigger(NoAnnotationTrigger.class);
}
@Test
public void badTriggerNameLoadTest() throws NoSuchFieldException, IllegalAccessException {
failIfHasTrigger(BadAnnotationTrigger.class);
}
@Test
public void duplicateTriggerLoadTest() throws NoSuchFieldException, IllegalAccessException {
Assertions.assertFalse(hasTrigger(DuplicateTriggerA.class) && hasTrigger(DuplicateTriggerB.class));
}
@Test
public void tryToGetNoExistTriggerTest() {
Assertions.assertThrows(NoSuchElementException.class, () ->
GroovyTriggerProvider.INSTANCE.getTriggerByName("NoExistTrigger"));
}
@Test
public void nullTest() {
Assertions.assertThrows(NullPointerException.class, () ->
GroovyTriggerProvider.INSTANCE.getTriggerByName(null));
}
private void failIfHasTrigger(Class<? extends GroovyTrigger> triggerClass)
throws NoSuchFieldException, IllegalAccessException {
if (hasTrigger(triggerClass)) {
Assertions.fail("Trigger did not appear as expected.");
}
}
@SuppressWarnings("unchecked")
private boolean hasTrigger(Class<? extends GroovyTrigger> triggerClass)
throws NoSuchFieldException, IllegalAccessException {
Field providerMapField =
GroovyTriggerProvider.class.getDeclaredField("triggerProviderMap");
providerMapField.setAccessible(true);
Map<String, ServiceLoader.Provider<GroovyTrigger>> map =
(Map<String, ServiceLoader.Provider<GroovyTrigger>>) providerMapField.get(
GroovyTriggerProvider.INSTANCE
);
providerMapField.setAccessible(false);
for (ServiceLoader.Provider<GroovyTrigger> value : map.values()) {
if (triggerClass.equals(value.type())) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,5 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
@TriggerName("")
public class BadAnnotationTrigger extends BaseTestTrigger {
}

View File

@ -0,0 +1,10 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import groovy.lang.Closure;
public abstract class BaseTestTrigger implements GroovyTrigger {
@Override
public void run(Closure<?> task) {
throw new UnsupportedOperationException("Unavailable trigger.");
}
}

View File

@ -0,0 +1,5 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
@TriggerName("Duplicate")
public class DuplicateTriggerA extends BaseTestTrigger {
}

View File

@ -0,0 +1,5 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
@TriggerName("Duplicate")
public class DuplicateTriggerB extends BaseTestTrigger {
}

View File

@ -0,0 +1,5 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
public class NoAnnotationTrigger extends BaseTestTrigger {
}

View File

@ -0,0 +1,4 @@
net.lamgc.oracle.sentry.script.groovy.trigger.NoAnnotationTrigger
net.lamgc.oracle.sentry.script.groovy.trigger.BadAnnotationTrigger
net.lamgc.oracle.sentry.script.groovy.trigger.DuplicateTriggerA
net.lamgc.oracle.sentry.script.groovy.trigger.DuplicateTriggerB

View File

@ -0,0 +1,7 @@
{
"username": "opc",
"authType": "password",
"port": "test",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,7 @@
{
"username": "opc",
"authType": "password",
"port": "-22",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,7 @@
{
"username": "opc",
"authType": "password",
"port": "1000000",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,9 @@
{
"username": {
},
"authType": "password",
"port": 22,
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,6 @@
{
"username": "opc",
"authType": "password",
"serverKey": "ssh-rsa AAAAaCADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,9 @@
{
"username": "opc",
"authType": "Public_Key",
"serverKey": {
"msg": "badServerKeyField"
},
"privateKeyPath": "~/.ssh/id_rsa",
"keyPassword": "123456"
}

View File

@ -0,0 +1,5 @@
{
"username": "opc",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,5 @@
{
"username": "opc",
"authType": "password",
"password": "123456"
}

View File

@ -0,0 +1,7 @@
{
"username": "opc",
"authType": "password",
"port": 22,
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"password": "123456"
}

View File

@ -0,0 +1,7 @@
{
"username": "opc",
"authType": "Public_Key",
"serverKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC/NGFFKkchNdE8HDE9WHGIcw97ZVOP5edY7drtRQn0xSSG6uLu08T36B8IWT+XJdg45/YMmcuVSzsG1QZs/R3s0URVUhsWjwdezWDeWeBHt8/6TGl2AsgA0iXSAOeRNldhZlITFvWoBEv2wElNjCTsEGo5bBp3rVPqqZNJFUs+FR9s/uVgmFqe7HGhuKhhk7BrRThJ/NcgDRicMQ4yXU3Hl++pG54TVLH+0HmgWg312XNAWtzw2iRmKBAuu2I4pP1TRp93K/lbD7QU8k8W7QcyGSAc73nZrhyzYVMko5wQGt4/vGpchOw7ehkotSejTB1GSyhzBTZobA23For76YLzuVFOjF3lEvSh1QV30ysu0PREKLtY83ad0WHVFqVgJrFHkkXQrglN335BhGwhFzwyMpRxbD8HCDtz6VjpqwoKtd/ExQkcfaj/g10o28vRzHGyzUbCTe433V61fjSsC4Bikw15vTnQ3ZuyOzfyoCYUNpFcf1Wv+mkoWqn9xU8lGvk= Test-Server",
"privateKeyPath": "~/.ssh/id_rsa",
"keyPassword": "123456"
}

View File

@ -0,0 +1,4 @@
{
"username": "linux",
"authType": "Unsupported"
}

View File

@ -0,0 +1,3 @@
[
"?"
]