From 273a5e78eb7056ee0159e1bd30a9675bb0026a21 Mon Sep 17 00:00:00 2001 From: JieguangZhou Date: Thu, 30 Mar 2023 20:17:28 +0800 Subject: [PATCH] [FEATURE][Task Plugin]Add remote-shell task plugin (#13801) --- docs/configs/docsdev.js | 16 ++ docs/docs/en/guide/datasource/ssh.md | 15 ++ docs/docs/en/guide/task/remoteshell.md | 31 +++ docs/docs/zh/guide/datasource/ssh.md | 15 ++ docs/docs/zh/guide/task/remoteshell.md | 30 +++ docs/img/new_ui/dev/datasource/ssh.png | Bin 0 -> 40549 bytes docs/img/tasks/demo/remote-shell.png | Bin 0 -> 78326 bytes docs/img/tasks/icons/remoteshell.png | Bin 0 -> 747 bytes .../service/impl/DataSourceServiceImpl.java | 15 +- .../src/main/resources/task-type-config.yaml | 1 + dolphinscheduler-bom/pom.xml | 12 + .../dolphinscheduler-datasource-all/pom.xml | 5 + .../api/datasource/DataSourceProcessor.java | 10 + .../dolphinscheduler-datasource-ssh/pom.xml | 47 ++++ .../datasource/ssh/SSHDataSourceChannel.java | 32 +++ .../ssh/SSHDataSourceChannelFactory.java | 37 +++ .../datasource/ssh/SSHDataSourceClient.java | 30 +++ .../plugin/datasource/ssh/SSHUtils.java | 62 +++++ .../ssh/param/SSHConnectionParam.java | 39 +++ .../ssh/param/SSHDataSourceParamDTO.java | 35 +++ .../ssh/param/SSHDataSourceProcessor.java | 139 ++++++++++ .../ssh/SSHDataSourceProcessorTest.java | 119 +++++++++ dolphinscheduler-datasource-plugin/pom.xml | 1 + dolphinscheduler-dist/release-docs/LICENSE | 3 + .../licenses/LICENSE-sshd-scp.txt | 202 ++++++++++++++ .../licenses/LICENSE-sshd-sftp.txt | 202 ++++++++++++++ .../spi/datasource/ConnectionParam.java | 8 + .../dolphinscheduler/spi/enums/DbType.java | 3 +- .../dolphinscheduler-task-all/pom.xml | 5 + .../dolphinscheduler-task-remoteshell/pom.xml | 57 ++++ .../task/remoteshell/RemoteExecutor.java | 251 ++++++++++++++++++ .../remoteshell/RemoteShellParameters.java | 50 ++++ .../task/remoteshell/RemoteShellTask.java | 185 +++++++++++++ .../remoteshell/RemoteShellTaskChannel.java | 48 ++++ .../RemoteShellTaskChannelFactory.java | 65 +++++ .../task/remoteshell/RemoteExecutorTest.java | 136 ++++++++++ .../task/remoteshell/RemoteShellTaskTest.java | 92 +++++++ dolphinscheduler-task-plugin/pom.xml | 1 + .../public/images/task-icons/remoteshell.png | Bin 0 -> 747 bytes .../images/task-icons/remoteshell_hover.png | Bin 0 -> 745 bytes .../src/service/modules/data-source/types.ts | 3 + .../src/store/project/task-type.ts | 4 + .../src/store/project/types.ts | 1 + .../src/views/datasource/list/detail.tsx | 18 ++ .../src/views/datasource/list/use-form.ts | 23 +- .../task/components/node/fields/index.ts | 1 + .../components/node/fields/use-datasource.ts | 7 +- .../node/fields/use-remote-shell.ts | 37 +++ .../task/components/node/format-data.ts | 5 + .../task/components/node/tasks/index.ts | 4 +- .../components/node/tasks/use-remote-shell.ts | 74 ++++++ .../projects/task/constants/task-type.ts | 5 + .../workflow/components/dag/dag.module.scss | 6 + tools/dependencies/known-dependencies.txt | 7 +- 54 files changed, 2187 insertions(+), 7 deletions(-) create mode 100644 docs/docs/en/guide/datasource/ssh.md create mode 100644 docs/docs/en/guide/task/remoteshell.md create mode 100644 docs/docs/zh/guide/datasource/ssh.md create mode 100644 docs/docs/zh/guide/task/remoteshell.md create mode 100644 docs/img/new_ui/dev/datasource/ssh.png create mode 100644 docs/img/tasks/demo/remote-shell.png create mode 100644 docs/img/tasks/icons/remoteshell.png create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/pom.xml create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannel.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceClient.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHConnectionParam.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceParamDTO.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/test/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceProcessorTest.java create mode 100644 dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-scp.txt create mode 100644 dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-sftp.txt create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/pom.xml create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellParameters.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTask.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannel.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannelFactory.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskTest.java create mode 100644 dolphinscheduler-ui/public/images/task-icons/remoteshell.png create mode 100644 dolphinscheduler-ui/public/images/task-icons/remoteshell_hover.png create mode 100644 dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-remote-shell.ts create mode 100644 dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-remote-shell.ts diff --git a/docs/configs/docsdev.js b/docs/configs/docsdev.js index 690d9ce883..1196850b5e 100644 --- a/docs/configs/docsdev.js +++ b/docs/configs/docsdev.js @@ -221,6 +221,10 @@ export default { title: 'Apache Linkis', link: '/en-us/docs/dev/user_doc/guide/task/linkis.html', }, + { + title: 'SSH', + link: '/en-us/docs/dev/user_doc/guide/task/ssh.html', + }, ], }, { @@ -319,6 +323,10 @@ export default { title: 'OceanBase', link: '/en-us/docs/dev/user_doc/guide/datasource/oceanbase.html', }, + { + title: 'SSH', + link: '/en-us/docs/dev/user_doc/guide/datasource/ssh.html', + }, ], }, { @@ -906,6 +914,10 @@ export default { title: 'Apache Linkis', link: '/zh-cn/docs/dev/user_doc/guide/task/linkis.html', }, + { + title: 'SSH', + link: '/zh-cn/docs/dev/user_doc/guide/task/ssh.html', + }, ], }, { @@ -988,6 +1000,10 @@ export default { title: 'OceanBase', link: '/zh-cn/docs/dev/user_doc/guide/datasource/oceanbase.html', }, + { + title: 'SSH', + link: '/zh-cn/docs/dev/user_doc/guide/datasource/ssh.html', + }, ], }, { diff --git a/docs/docs/en/guide/datasource/ssh.md b/docs/docs/en/guide/datasource/ssh.md new file mode 100644 index 0000000000..0850ca1c18 --- /dev/null +++ b/docs/docs/en/guide/datasource/ssh.md @@ -0,0 +1,15 @@ +# SSH Data Source + +This data source is used for RemoteShell component to execute commands remotely. + +![sh](../../../../img/new_ui/dev/datasource/ssh.png) + +- Data Source: SSH +- Data Source Name: Enter the name of the data source +- Description: Enter the description of the data source +- IP Hostname: Enter the IP to connect to SSH +- Port: Enter the port to connect to SSH +- Username: Set the username to connect to SSH +- Password: Set the password to connect to SSH +- Public Key: Set the public key to connect to SSH + diff --git a/docs/docs/en/guide/task/remoteshell.md b/docs/docs/en/guide/task/remoteshell.md new file mode 100644 index 0000000000..0286e2f825 --- /dev/null +++ b/docs/docs/en/guide/task/remoteshell.md @@ -0,0 +1,31 @@ +# RemoteShell + +## Overview + +RemoteShell task type is used to execute commands on remote servers. + +## Create Task + +- Click Project Management-Project Name-Workflow Definition, click the "Create Workflow" button to enter the DAG editing page. + +- Drag from the toolbar to the canvas to complete the creation. + +## Task Parameters + +[//]: # (TODO: use the commented anchor below once our website template supports this syntax) +[//]: # (- Please refer to [DolphinScheduler Task Parameters Appendix](appendix.md#default-task-parameters) `Default Task Parameters` section for default parameters.) + +- Please refer to [DolphinScheduler Task Parameters Appendix](appendix.md) `Default Task Parameters` section for default parameters. +- SSH Data Source: Select SSH data source. + +## Task Example + +### View the path of the remote server (remote-server) + +![remote-shell-demo](../../../../img/tasks/demo/remote-shell.png) + +## Precautions + +After the task connects to the server, it will not automatically source bashrc and other files. The required environment variables can be imported in the following ways +- Create environment variables in the security center-Environment Management, and then import them through the environment option in the task definition +- Enter the corresponding environment variables directly in the script diff --git a/docs/docs/zh/guide/datasource/ssh.md b/docs/docs/zh/guide/datasource/ssh.md new file mode 100644 index 0000000000..f6f6f220f9 --- /dev/null +++ b/docs/docs/zh/guide/datasource/ssh.md @@ -0,0 +1,15 @@ +# SSH 数据源 + +该数据源用于RemoteShell组件,用于远程执行命令。 + +![sh](../../../../img/new_ui/dev/datasource/ssh.png) + +- 数据源:选择 SSH +- 数据源名称:输入数据源的名称 +- 描述:输入数据源的描述 +- IP 主机名:输入连接 SSH 的 IP +- 端口:输入连接 SSH 的端口 +- 用户名:设置连接 SSH 的用户名 +- 密码:设置连接 SSH 的密码 +- 公钥:设置连接 SSH 的公钥 + diff --git a/docs/docs/zh/guide/task/remoteshell.md b/docs/docs/zh/guide/task/remoteshell.md new file mode 100644 index 0000000000..67735dca63 --- /dev/null +++ b/docs/docs/zh/guide/task/remoteshell.md @@ -0,0 +1,30 @@ +# RemoteShell + +## 综述 + +RemoteShell 任务类型,用于在远程服务器上执行命令。 + +## 创建任务 + +- 点击项目管理-项目名称-工作流定义,点击"创建工作流"按钮,进入 DAG 编辑页面。 +- 工具栏中拖动 到画板中,即可完成创建。 + +## 任务参数 + +[//]: # (TODO: use the commented anchor below once our website template supports this syntax) +[//]: # (- 默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md#默认任务参数)`默认任务参数`一栏。) + +- 默认参数说明请参考[DolphinScheduler任务参数附录](appendix.md)`默认任务参数`一栏。 +- SSH Data Source: 选择SSH 数据源。 + +## 任务样例 + +### 查看远程服务器(remote-server)的路径 + +![remote-shell-demo](../../../../img/tasks/demo/remote-shell.png) + +## 注意事项 + +该任务连接服务器后,不会自动source bashrc等文件,所需的环境变量,可以通过以下方式导入 +- 在安全中心-环境管理中创建环境变量,然后通过任务定义中的环境选项引入 +- 在脚本中直接输入对应的环境变量 diff --git a/docs/img/new_ui/dev/datasource/ssh.png b/docs/img/new_ui/dev/datasource/ssh.png new file mode 100644 index 0000000000000000000000000000000000000000..e29d231bdd4bbd70937e1feddc76aeca8e1a4e65 GIT binary patch literal 40549 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 literal 0 HcmV?d00001 diff --git a/docs/img/tasks/demo/remote-shell.png b/docs/img/tasks/demo/remote-shell.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e6773acb0f48d4373123b88c47ff2564d41bf2 GIT binary patch literal 78326 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μ?|KC*~N^y`$7`hw%K{U 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 OFnGH9xvXdxEk6 literal 0 HcmV?d00001 diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java index 6f5c8e5eca..fc39e1b72d 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/DataSourceServiceImpl.java @@ -35,6 +35,7 @@ import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.DataSourceMapper; import org.apache.dolphinscheduler.dao.mapper.DataSourceUserMapper; import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.DataSourceProcessor; import org.apache.dolphinscheduler.plugin.datasource.api.plugin.DataSourceClientProvider; import org.apache.dolphinscheduler.plugin.datasource.api.utils.DataSourceUtils; import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; @@ -186,9 +187,10 @@ public class DataSourceServiceImpl extends BaseServiceImpl implements DataSource return result; } // check password,if the password is not updated, set to the old password. - BaseConnectionParam connectionParam = - (BaseConnectionParam) DataSourceUtils.buildConnectionParams(dataSourceParam); + ConnectionParam connectionParam = DataSourceUtils.buildConnectionParams(dataSourceParam); + String password = connectionParam.getPassword(); + if (StringUtils.isBlank(password)) { String oldConnectionParams = dataSource.getConnectionParams(); ObjectNode oldParams = JSONUtils.parseObject(oldConnectionParams); @@ -383,6 +385,15 @@ public class DataSourceServiceImpl extends BaseServiceImpl implements DataSource @Override public Result checkConnection(DbType type, ConnectionParam connectionParam) { Result result = new Result<>(); + if (type == DbType.SSH) { + DataSourceProcessor sshDataSourceProcessor = DataSourceUtils.getDatasourceProcessor(type); + if (sshDataSourceProcessor.testConnection(connectionParam)) { + putMsg(result, Status.SUCCESS); + } else { + putMsg(result, Status.CONNECT_DATASOURCE_FAILURE); + } + return result; + } try (Connection connection = DataSourceClientProvider.getInstance().getConnection(type, connectionParam)) { if (connection == null) { log.error("Connection test to {} datasource failed, connectionParam:{}.", type.getDescp(), diff --git a/dolphinscheduler-api/src/main/resources/task-type-config.yaml b/dolphinscheduler-api/src/main/resources/task-type-config.yaml index 688f02930d..7a21c36946 100644 --- a/dolphinscheduler-api/src/main/resources/task-type-config.yaml +++ b/dolphinscheduler-api/src/main/resources/task-type-config.yaml @@ -29,6 +29,7 @@ task: - 'DINKY' - 'FLINK_STREAM' - 'HIVECLI' + - 'REMOTESHELL' cloud: - 'EMR' - 'K8S' diff --git a/dolphinscheduler-bom/pom.xml b/dolphinscheduler-bom/pom.xml index a2a2ed90a3..458bf64ad7 100644 --- a/dolphinscheduler-bom/pom.xml +++ b/dolphinscheduler-bom/pom.xml @@ -106,6 +106,7 @@ 2.21.0 1.0.0-beta.19 2.18.0 + 2.8.0 @@ -738,6 +739,17 @@ + + org.apache.sshd + sshd-sftp + ${sshd.version} + + + org.apache.sshd + sshd-scp + ${sshd.version} + + org.apache.spark spark-sql_2.12 diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml index 28b2f21ec9..010343bde8 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml @@ -107,5 +107,10 @@ dolphinscheduler-datasource-dameng ${project.version} + + org.apache.dolphinscheduler + dolphinscheduler-datasource-ssh + ${project.version} + diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/DataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/DataSourceProcessor.java index e30a638125..170b391c45 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/DataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-api/src/main/java/org/apache/dolphinscheduler/plugin/datasource/api/datasource/DataSourceProcessor.java @@ -95,6 +95,16 @@ public interface DataSourceProcessor { */ Connection getConnection(ConnectionParam connectionParam) throws ClassNotFoundException, SQLException, IOException; + /** + * test connection, use for not jdbc datasource + * + * @param connectionParam connectionParam + * @return true if connection is valid + */ + default boolean testConnection(ConnectionParam connectionParam) { + return false; + } + /** * @return {@link DbType} */ diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/pom.xml b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/pom.xml new file mode 100644 index 0000000000..6645ee8a90 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-datasource-plugin + dev-SNAPSHOT + + + dolphinscheduler-datasource-ssh + jar + ${project.artifactId} + + + + org.apache.dolphinscheduler + dolphinscheduler-spi + provided + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-api + ${project.version} + + + org.apache.sshd + sshd-scp + + + diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannel.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannel.java new file mode 100644 index 0000000000..73d7228979 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannel.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh; + +import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; +import org.apache.dolphinscheduler.spi.datasource.DataSourceClient; +import org.apache.dolphinscheduler.spi.enums.DbType; + +public class SSHDataSourceChannel implements DataSourceChannel { + + @Override + public DataSourceClient createDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + return new SSHDataSourceClient(baseConnectionParam, dbType); + } + +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java new file mode 100644 index 0000000000..3195432703 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceChannelFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh; + +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; + +import com.google.auto.service.AutoService; + +@AutoService(DataSourceChannelFactory.class) +public class SSHDataSourceChannelFactory implements DataSourceChannelFactory { + + @Override + public String getName() { + return "ssh"; + } + + @Override + public DataSourceChannel create() { + return new SSHDataSourceChannel(); + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceClient.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceClient.java new file mode 100644 index 0000000000..fd9ce7d646 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceClient.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh; + +import org.apache.dolphinscheduler.plugin.datasource.api.client.CommonDataSourceClient; +import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +public class SSHDataSourceClient extends CommonDataSourceClient { + + public SSHDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + super(baseConnectionParam, dbType); + } + +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java new file mode 100644 index 0000000000..8ee8fa79a7 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHUtils.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh; + +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader; +import org.apache.sshd.common.util.security.SecurityUtils; + +import java.security.KeyPair; +import java.util.Collection; + +public class SSHUtils { + + private SSHUtils() { + throw new IllegalStateException("Utility class"); + } + + public static ClientSession getSession(SshClient client, SSHConnectionParam connectionParam) throws Exception { + ClientSession session; + session = client.connect(connectionParam.getUser(), connectionParam.getHost(), connectionParam.getPort()) + .verify(5000).getSession(); + // add password identity + String password = connectionParam.getPassword(); + if (StringUtils.isNotEmpty(password)) { + session.addPasswordIdentity(password); + } + + // add public key identity + String publicKey = connectionParam.getPublicKey(); + if (StringUtils.isNotEmpty(publicKey)) { + try { + KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser(); + Collection keyPairCollection = loader.loadKeyPairs(null, null, null, publicKey); + for (KeyPair keyPair : keyPairCollection) { + session.addPublicKeyIdentity(keyPair); + } + } catch (Exception e) { + throw new Exception("Failed to add public key identity", e); + } + } + return session; + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHConnectionParam.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHConnectionParam.java new file mode 100644 index 0000000000..06206ca030 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHConnectionParam.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh.param; + +import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; + +import lombok.Data; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SSHConnectionParam implements ConnectionParam { + + protected String user; + + protected String password; + + protected String publicKey; + + protected String host; + + protected int port = 22; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceParamDTO.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceParamDTO.java new file mode 100644 index 0000000000..2773b12b88 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceParamDTO.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh.param; + +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import lombok.Data; + +@Data +public class SSHDataSourceParamDTO extends BaseDataSourceParamDTO { + + protected String publicKey; + + @Override + public DbType getType() { + return DbType.SSH; + + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java new file mode 100644 index 0000000000..37dee2979c --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/main/java/org/apache/dolphinscheduler/plugin/datasource/ssh/param/SSHDataSourceProcessor.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh.param; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.DataSourceProcessor; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; +import org.apache.dolphinscheduler.plugin.datasource.ssh.SSHUtils; +import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; + +import java.sql.Connection; +import java.text.MessageFormat; + +import lombok.extern.slf4j.Slf4j; + +import com.google.auto.service.AutoService; + +@AutoService(DataSourceProcessor.class) +@Slf4j +public class SSHDataSourceProcessor implements DataSourceProcessor { + + @Override + public BaseDataSourceParamDTO castDatasourceParamDTO(String paramJson) { + return JSONUtils.parseObject(paramJson, SSHDataSourceParamDTO.class); + } + + @Override + public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { + if (StringUtils.isEmpty(datasourceParamDTO.getHost()) + || StringUtils.isEmpty(datasourceParamDTO.getUserName())) { + throw new IllegalArgumentException("ssh datasource param is not valid"); + } + } + + @Override + public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { + SSHConnectionParam baseConnectionParam = (SSHConnectionParam) connectionParam; + return MessageFormat.format("{0}@{1}@{2}@{3}", dbType.getDescp(), baseConnectionParam.getHost(), + baseConnectionParam.getUser(), + PasswordUtils.encodePassword(baseConnectionParam.getPassword())); + } + + @Override + public BaseDataSourceParamDTO createDatasourceParamDTO(String connectionJson) { + SSHConnectionParam connectionParams = (SSHConnectionParam) createConnectionParams(connectionJson); + SSHDataSourceParamDTO sshDataSourceParamDTO = new SSHDataSourceParamDTO(); + + sshDataSourceParamDTO.setUserName(connectionParams.getUser()); + sshDataSourceParamDTO.setPassword(connectionParams.getPassword()); + sshDataSourceParamDTO.setHost(connectionParams.getHost()); + sshDataSourceParamDTO.setPort(connectionParams.getPort()); + sshDataSourceParamDTO.setPublicKey(connectionParams.getPublicKey()); + + return sshDataSourceParamDTO; + } + + @Override + public SSHConnectionParam createConnectionParams(BaseDataSourceParamDTO dataSourceParam) { + SSHDataSourceParamDTO sshDataSourceParam = (SSHDataSourceParamDTO) dataSourceParam; + SSHConnectionParam sshConnectionParam = new SSHConnectionParam(); + sshConnectionParam.setUser(sshDataSourceParam.getUserName()); + sshConnectionParam.setPassword(sshDataSourceParam.getPassword()); + sshConnectionParam.setHost(sshDataSourceParam.getHost()); + sshConnectionParam.setPort(sshDataSourceParam.getPort()); + sshConnectionParam.setPublicKey(sshDataSourceParam.getPublicKey()); + + return sshConnectionParam; + } + + @Override + public ConnectionParam createConnectionParams(String connectionJson) { + return JSONUtils.parseObject(connectionJson, SSHConnectionParam.class); + } + + @Override + public String getDatasourceDriver() { + return ""; + } + + @Override + public String getValidationQuery() { + return ""; + } + + @Override + public String getJdbcUrl(ConnectionParam connectionParam) { + return ""; + } + + @Override + public Connection getConnection(ConnectionParam connectionParam) { + return null; + } + + @Override + public boolean testConnection(ConnectionParam connectionParam) { + SSHConnectionParam baseConnectionParam = (SSHConnectionParam) connectionParam; + SshClient client = SshClient.setUpDefaultClient(); + client.start(); + try { + ClientSession session = SSHUtils.getSession(client, baseConnectionParam); + return session.auth().verify().isSuccess(); + } catch (Exception e) { + return false; + } + } + + @Override + public DbType getDbType() { + return DbType.SSH; + } + + @Override + public DataSourceProcessor create() { + return new SSHDataSourceProcessor(); + } + +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/test/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceProcessorTest.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/test/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceProcessorTest.java new file mode 100644 index 0000000000..51aee19edd --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-ssh/src/test/java/org/apache/dolphinscheduler/plugin/datasource/ssh/SSHDataSourceProcessorTest.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.ssh; + +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceProcessor; +import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import org.apache.sshd.client.session.ClientSession; + +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SSHDataSourceProcessorTest { + + private SSHDataSourceProcessor sshDataSourceProcessor; + + private String connectJson = + "{\"user\":\"lucky\",\"password\":\"123456\",\"host\":\"dolphinscheduler.com\",\"port\":22, \"publicKey\":\"ssh-rsa AAAAB\"}"; + + @BeforeEach + public void init() { + sshDataSourceProcessor = new SSHDataSourceProcessor(); + } + + @Test + void testCheckDatasourceParam() { + BaseDataSourceParamDTO baseDataSourceParamDTO = new SSHDataSourceParamDTO(); + Assertions.assertThrows(IllegalArgumentException.class, + () -> sshDataSourceProcessor.checkDatasourceParam(baseDataSourceParamDTO)); + baseDataSourceParamDTO.setHost("localhost"); + Assertions.assertThrows(IllegalArgumentException.class, + () -> sshDataSourceProcessor.checkDatasourceParam(baseDataSourceParamDTO)); + baseDataSourceParamDTO.setUserName("root"); + Assertions.assertDoesNotThrow(() -> sshDataSourceProcessor.checkDatasourceParam(baseDataSourceParamDTO)); + + } + + @Test + void testGetDatasourceUniqueId() { + SSHConnectionParam sshConnectionParam = new SSHConnectionParam(); + sshConnectionParam.setHost("localhost"); + sshConnectionParam.setUser("root"); + sshConnectionParam.setPassword("123456"); + Assertions.assertEquals("ssh@localhost@root@123456", + sshDataSourceProcessor.getDatasourceUniqueId(sshConnectionParam, DbType.SSH)); + + } + + @Test + void testCreateDatasourceParamDTO() { + SSHDataSourceParamDTO sshDataSourceParamDTO = + (SSHDataSourceParamDTO) sshDataSourceProcessor.createDatasourceParamDTO(connectJson); + Assertions.assertEquals("lucky", sshDataSourceParamDTO.getUserName()); + Assertions.assertEquals("123456", sshDataSourceParamDTO.getPassword()); + Assertions.assertEquals("dolphinscheduler.com", sshDataSourceParamDTO.getHost()); + Assertions.assertEquals(22, sshDataSourceParamDTO.getPort()); + Assertions.assertEquals("ssh-rsa AAAAB", sshDataSourceParamDTO.getPublicKey()); + } + + @Test + void testCreateConnectionParams() { + SSHDataSourceParamDTO sshDataSourceParamDTO = + (SSHDataSourceParamDTO) sshDataSourceProcessor.createDatasourceParamDTO(connectJson); + SSHConnectionParam sshConnectionParam = sshDataSourceProcessor.createConnectionParams(sshDataSourceParamDTO); + Assertions.assertEquals("lucky", sshConnectionParam.getUser()); + Assertions.assertEquals("123456", sshConnectionParam.getPassword()); + Assertions.assertEquals("dolphinscheduler.com", sshConnectionParam.getHost()); + Assertions.assertEquals(22, sshConnectionParam.getPort()); + Assertions.assertEquals("ssh-rsa AAAAB", sshConnectionParam.getPublicKey()); + } + + @Test + void testTestConnection() throws IOException { + SSHDataSourceParamDTO sshDataSourceParamDTO = + (SSHDataSourceParamDTO) sshDataSourceProcessor.createDatasourceParamDTO(connectJson); + ConnectionParam connectionParam = sshDataSourceProcessor.createConnectionParams(sshDataSourceParamDTO); + MockedStatic sshConnectionUtilsMockedStatic = org.mockito.Mockito.mockStatic(SSHUtils.class); + sshConnectionUtilsMockedStatic.when(() -> SSHUtils.getSession(Mockito.any(), Mockito.any())).thenReturn(null); + Assertions.assertFalse(sshDataSourceProcessor.testConnection(connectionParam)); + + ClientSession clientSession = Mockito.mock(ClientSession.class, RETURNS_DEEP_STUBS); + sshConnectionUtilsMockedStatic.when(() -> SSHUtils.getSession(Mockito.any(), Mockito.any())) + .thenReturn(clientSession); + when(clientSession.auth().verify().isSuccess()).thenReturn(true); + Assertions.assertTrue(sshDataSourceProcessor.testConnection(connectionParam)); + + } + +} diff --git a/dolphinscheduler-datasource-plugin/pom.xml b/dolphinscheduler-datasource-plugin/pom.xml index 767d54b449..53bfa9e009 100644 --- a/dolphinscheduler-datasource-plugin/pom.xml +++ b/dolphinscheduler-datasource-plugin/pom.xml @@ -46,6 +46,7 @@ dolphinscheduler-datasource-starrocks dolphinscheduler-datasource-azure-sql dolphinscheduler-datasource-dameng + dolphinscheduler-datasource-ssh diff --git a/dolphinscheduler-dist/release-docs/LICENSE b/dolphinscheduler-dist/release-docs/LICENSE index b3a895f6cf..6bdca226e3 100644 --- a/dolphinscheduler-dist/release-docs/LICENSE +++ b/dolphinscheduler-dist/release-docs/LICENSE @@ -547,6 +547,9 @@ The text of each license is also included at licenses/LICENSE-[project].txt. opencensus-proto 0.2.0: https://mvnrepository.com/artifact/io.opencensus/opencensus-proto/0.2.0, Apache 2.0 proto-google-cloud-storage-v2 2.18.0-alpha: https://mvnrepository.com/artifact/com.google.api.grpc/proto-google-cloud-storage-v2/2.18.0-alpha, Apache 2.0 proto-google-iam-v1 1.9.0: https://mvnrepository.com/artifact/com.google.api.grpc/proto-google-iam-v1/1.9.0, Apache 2.0 + sshd-sftp https://mvnrepository.com/artifact/org.apache.sshd/sshd-sftp/2.8.0 Apache 2.0 + sshd-scp https://mvnrepository.com/artifact/org.apache.sshd/sshd-scp/2.8.0 Aapache 2.0 + jna-platform diff --git a/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-scp.txt b/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-scp.txt new file mode 100644 index 0000000000..57bc88a15a --- /dev/null +++ b/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-scp.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-sftp.txt b/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-sftp.txt new file mode 100644 index 0000000000..57bc88a15a --- /dev/null +++ b/dolphinscheduler-dist/release-docs/licenses/LICENSE-sshd-sftp.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/datasource/ConnectionParam.java b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/datasource/ConnectionParam.java index b3eb903dc7..0f408cae18 100644 --- a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/datasource/ConnectionParam.java +++ b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/datasource/ConnectionParam.java @@ -23,4 +23,12 @@ import java.io.Serializable; * The model of Datasource Connection param */ public interface ConnectionParam extends Serializable { + + default String getPassword() { + return ""; + } + + default void setPassword(String s) { + } + } diff --git a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java index 7eb8855b73..90d556feaf 100644 --- a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java +++ b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java @@ -44,7 +44,8 @@ public enum DbType { STARROCKS(13, "starrocks"), AZURESQL(14, "azuresql"), DAMENG(15, "dameng"), - OCEANBASE(16, "oceanbase"); + OCEANBASE(16, "oceanbase"), + SSH(17, "ssh"); private static final Map DB_TYPE_MAP = Arrays.stream(DbType.values()).collect(toMap(DbType::getCode, Functions.identity())); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml index 9d11d6fe58..5e4c74b27a 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml @@ -217,6 +217,11 @@ dolphinscheduler-task-datafactory ${project.version} + + org.apache.dolphinscheduler + dolphinscheduler-task-remoteshell + ${project.version} + diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/pom.xml new file mode 100644 index 0000000000..3ff18edb9c --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-task-plugin + dev-SNAPSHOT + + + dolphinscheduler-task-remoteshell + jar + + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-all + + + + org.apache.dolphinscheduler + dolphinscheduler-spi + provided + + + org.apache.dolphinscheduler + dolphinscheduler-task-api + ${project.version} + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-all + ${project.version} + + + org.apache.sshd + sshd-sftp + + + + diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java new file mode 100644 index 0000000000..650bf84a69 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutor.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import org.apache.dolphinscheduler.plugin.datasource.ssh.SSHUtils; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; +import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.sftp.client.SftpClientFactory; +import org.apache.sshd.sftp.client.fs.SftpFileSystem; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.EnumSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RemoteExecutor { + + protected final Logger logger = + LoggerFactory.getLogger(String.format(TaskConstants.TASK_LOGGER_THREAD_NAME, getClass())); + + protected static final Pattern SETVALUE_REGEX = Pattern.compile(TaskConstants.SETVALUE_REGEX); + + static final String REMOTE_SHELL_HOME = "/tmp/dolphinscheduler-remote-shell-%s/"; + static final String STATUS_TAG_MESSAGE = "DOLPHINSCHEDULER-REMOTE-SHELL-TASK-STATUS-"; + static final int TRACK_INTERVAL = 5000; + + protected StringBuilder varPool = new StringBuilder(); + + SshClient sshClient; + ClientSession session; + SSHConnectionParam sshConnectionParam; + + public RemoteExecutor(SSHConnectionParam sshConnectionParam) { + + this.sshConnectionParam = sshConnectionParam; + initClient(); + } + + private void initClient() { + sshClient = SshClient.setUpDefaultClient(); + sshClient.start(); + } + + private ClientSession getSession() { + if (session != null && session.isOpen()) { + return session; + } + try { + session = SSHUtils.getSession(sshClient, sshConnectionParam); + if (session == null || !session.auth().verify().isSuccess()) { + throw new TaskException("SSH connection failed"); + } + } catch (Exception e) { + throw new TaskException("SSH connection failed", e); + } + return session; + } + + public int run(String taskId, String localFile) throws IOException { + try { + // only run task if no exist same task + String pid = getTaskPid(taskId); + if (StringUtils.isEmpty(pid)) { + saveCommand(taskId, localFile); + String runCommand = String.format(COMMAND.RUN_COMMAND, getRemoteShellHome(), taskId, + getRemoteShellHome(), taskId); + runRemote(runCommand); + } + track(taskId); + return getTaskExitCode(taskId); + } catch (Exception e) { + throw new TaskException("Remote shell task error", e); + } + } + + public void track(String taskId) throws Exception { + int logN = 0; + String pid; + logger.info("Remote shell task log:"); + do { + pid = getTaskPid(taskId); + String trackCommand = String.format(COMMAND.TRACK_COMMAND, logN + 1, getRemoteShellHome(), taskId); + String log = runRemote(trackCommand); + if (StringUtils.isEmpty(log)) { + Thread.sleep(TRACK_INTERVAL); + } else { + logN += log.split("\n").length; + setVarPool(log); + logger.info(log); + } + } while (StringUtils.isNotEmpty(pid)); + } + + public String getVarPool() { + return varPool.toString(); + } + + private void setVarPool(String log) { + String[] lines = log.split("\n"); + for (String line : lines) { + if (line.startsWith("${setValue(") || line.startsWith("#{setValue(")) { + varPool.append(findVarPool(line)); + varPool.append("$VarPool$"); + } + } + } + + private String findVarPool(String line) { + Matcher matcher = SETVALUE_REGEX.matcher(line); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + public Integer getTaskExitCode(String taskId) throws IOException { + String trackCommand = String.format(COMMAND.LOG_TAIL_COMMAND, getRemoteShellHome(), taskId); + String log = runRemote(trackCommand); + int exitCode = -1; + logger.info("Remote shell task run status: {}", log); + if (log.contains(STATUS_TAG_MESSAGE)) { + String status = log.replace(STATUS_TAG_MESSAGE, "").trim(); + if (status.equals("0")) { + logger.info("Remote shell task success"); + exitCode = 0; + } else { + logger.error("Remote shell task failed"); + exitCode = Integer.parseInt(status); + } + } + cleanData(taskId); + logger.error("Remote shell task failed"); + return exitCode; + } + + public void cleanData(String taskId) { + String cleanCommand = + String.format(COMMAND.CLEAN_COMMAND, getRemoteShellHome(), taskId, getRemoteShellHome(), taskId); + try { + runRemote(cleanCommand); + } catch (Exception e) { + logger.error("Remote shell task clean data failed, but will not affect the task execution", e); + } + } + + public void kill(String taskId) throws IOException { + String pid = getTaskPid(taskId); + String killCommand = String.format(COMMAND.KILL_COMMAND, pid); + runRemote(killCommand); + cleanData(taskId); + } + + public String getTaskPid(String taskId) throws IOException { + String pidCommand = String.format(COMMAND.GET_PID_COMMAND, taskId); + return runRemote(pidCommand).trim(); + } + + public void saveCommand(String taskId, String localFile) throws IOException { + String checkDirCommand = String.format(COMMAND.CHECK_DIR, getRemoteShellHome(), getRemoteShellHome()); + runRemote(checkDirCommand); + uploadScript(taskId, localFile); + + logger.info("The final script is: \n{}", + runRemote(String.format(COMMAND.CAT_FINAL_SCRIPT, getRemoteShellHome(), taskId))); + } + + public void uploadScript(String taskId, String localFile) throws IOException { + + String remotePath = getRemoteShellHome() + taskId + ".sh"; + logger.info("upload script from local:{} to remote: {}", localFile, remotePath); + try (SftpFileSystem fs = SftpClientFactory.instance().createSftpFileSystem(getSession())) { + Path path = fs.getPath(remotePath); + Files.copy(Paths.get(localFile), path); + } + } + + public String runRemote(String command) throws IOException { + try ( + ChannelExec channel = getSession().createExecChannel(command); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream()) { + + channel.setOut(System.out); + channel.setOut(out); + channel.setErr(err); + channel.open(); + channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0); + channel.close(); + if (channel.getExitStatus() != 0) { + throw new TaskException("Remote shell task error, error message: " + err.toString()); + } + return out.toString(); + } + } + + private String getRemoteShellHome() { + return String.format(REMOTE_SHELL_HOME, sshConnectionParam.getUser()); + } + + static class COMMAND { + + private COMMAND() { + throw new IllegalStateException("Utility class"); + } + + static final String CHECK_DIR = "if [ ! -d %s ]; then mkdir -p %s; fi"; + static final String RUN_COMMAND = "nohup /bin/bash %s%s.sh >%s%s.log 2>&1 &"; + static final String TRACK_COMMAND = "tail -n +%s %s%s.log"; + + static final String LOG_TAIL_COMMAND = "tail -n 1 %s%s.log"; + static final String GET_PID_COMMAND = "ps -ef | grep \"%s.sh\" | grep -v grep | awk '{print $2}'"; + static final String KILL_COMMAND = "kill -9 %s"; + static final String CLEAN_COMMAND = "rm %s%s.sh %s%s.log"; + + static final String HEADER = "#!/bin/bash\n"; + + static final String ADD_STATUS_COMMAND = "\necho %s$?"; + + static final String CAT_FINAL_SCRIPT = "cat %s%s.sh"; + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellParameters.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellParameters.java new file mode 100644 index 0000000000..f0b5befe04 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellParameters.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper; + +import lombok.Data; + +@Data +public class RemoteShellParameters extends AbstractParameters { + + private String rawScript; + + private String type; + + /** + * datasource id + */ + private int datasource; + + @Override + public boolean checkParameters() { + return rawScript != null && !rawScript.isEmpty(); + } + + @Override + public ResourceParametersHelper getResources() { + ResourceParametersHelper resources = super.getResources(); + resources.put(ResourceType.DATASOURCE, datasource); + return resources; + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTask.java new file mode 100644 index 0000000000..48644d7f3c --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTask.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import static org.apache.dolphinscheduler.plugin.task.api.TaskConstants.EXIT_CODE_FAILURE; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.DataSourceUtils; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; +import org.apache.dolphinscheduler.plugin.task.api.AbstractTask; +import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters; +import org.apache.dolphinscheduler.plugin.task.api.parser.ParamUtils; +import org.apache.dolphinscheduler.plugin.task.api.parser.ParameterUtils; +import org.apache.dolphinscheduler.plugin.task.api.utils.FileUtils; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import org.apache.commons.lang3.SystemUtils; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; + +/** + * shell task + */ +public class RemoteShellTask extends AbstractTask { + + static final String TASK_ID_PREFIX = "dolphinscheduler-remoteshell-"; + + /** + * shell parameters + */ + private RemoteShellParameters remoteShellParameters; + + /** + * taskExecutionContext + */ + private TaskExecutionContext taskExecutionContext; + + private RemoteExecutor remoteExecutor; + + private String taskId; + + /** + * constructor + * + * @param taskExecutionContext taskExecutionContext + */ + public RemoteShellTask(TaskExecutionContext taskExecutionContext) { + super(taskExecutionContext); + + this.taskExecutionContext = taskExecutionContext; + } + + @Override + public void init() { + log.info("shell task params {}", taskExecutionContext.getTaskParams()); + + remoteShellParameters = + JSONUtils.parseObject(taskExecutionContext.getTaskParams(), RemoteShellParameters.class); + + if (!remoteShellParameters.checkParameters()) { + throw new TaskException("sell task params is not valid"); + } + + taskId = taskExecutionContext.getAppIds(); + if (taskId == null) { + taskId = TASK_ID_PREFIX + taskExecutionContext.getTaskInstanceId(); + } + setAppIds(taskId); + taskExecutionContext.setAppIds(taskId); + + initRemoteExecutor(); + } + + @Override + public void handle(TaskCallBack taskCallBack) throws TaskException { + try { + // construct process + String localFile = buildCommand(); + int exitCode = remoteExecutor.run(taskId, localFile); + setExitStatusCode(exitCode); + remoteShellParameters.dealOutParam(remoteExecutor.getVarPool()); + } catch (Exception e) { + log.error("shell task error", e); + setExitStatusCode(EXIT_CODE_FAILURE); + throw new TaskException("Execute shell task error", e); + } + } + + @Override + public void cancel() throws TaskException { + // cancel process + try { + log.info("kill remote task {}", taskId); + remoteExecutor.kill(taskId); + } catch (Exception e) { + throw new TaskException("cancel application error", e); + } + } + + /** + * create command + * + * @return file name + * @throws Exception exception + */ + public String buildCommand() throws Exception { + // generate scripts + String fileName = String.format("%s/%s_node.%s", + taskExecutionContext.getExecutePath(), + taskExecutionContext.getTaskAppId(), SystemUtils.IS_OS_WINDOWS ? "bat" : "sh"); + + File file = new File(fileName); + Path path = file.toPath(); + + if (Files.exists(path)) { + // this shouldn't happen + log.warn("The command file: {} is already exist", path); + return fileName; + } + + String script = remoteShellParameters.getRawScript().replaceAll("\\r\\n", "\n"); + script = parseScript(script); + + String environment = taskExecutionContext.getEnvironmentConfig(); + if (environment != null) { + environment = environment.replaceAll("\\r\\n", "\n"); + environment = environment.replace("\r\n", "\n"); + script = environment + "\n" + script; + } + script = String.format(RemoteExecutor.COMMAND.HEADER) + script; + script += String.format(RemoteExecutor.COMMAND.ADD_STATUS_COMMAND, RemoteExecutor.STATUS_TAG_MESSAGE); + + FileUtils.createFileWith755(path); + Files.write(path, script.getBytes(), StandardOpenOption.APPEND); + log.info("raw script : {}", script); + return fileName; + } + + @Override + public AbstractParameters getParameters() { + return remoteShellParameters; + } + + private String parseScript(String script) { + // combining local and global parameters + Map paramsMap = taskExecutionContext.getPrepareParamsMap(); + return ParameterUtils.convertParameterPlaceholders(script, ParamUtils.convert(paramsMap)); + } + + public void initRemoteExecutor() { + DataSourceParameters dbSource = (DataSourceParameters) taskExecutionContext.getResourceParametersHelper() + .getResourceParameters(ResourceType.DATASOURCE, remoteShellParameters.getDatasource()); + taskExecutionContext.getResourceParametersHelper().getResourceParameters(ResourceType.DATASOURCE, + remoteShellParameters.getDatasource()); + SSHConnectionParam sshConnectionParam = (SSHConnectionParam) DataSourceUtils.buildConnectionParams( + DbType.valueOf(remoteShellParameters.getType()), + dbSource.getConnectionParams()); + remoteExecutor = new RemoteExecutor(sshConnectionParam); + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannel.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannel.java new file mode 100644 index 0000000000..e2baeca3d1 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannel.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.task.api.TaskChannel; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.ParametersNode; +import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper; + +public class RemoteShellTaskChannel implements TaskChannel { + + @Override + public void cancelApplication(boolean status) { + + } + + @Override + public RemoteShellTask createTask(TaskExecutionContext taskRequest) { + return new RemoteShellTask(taskRequest); + } + + @Override + public AbstractParameters parseParameters(ParametersNode parametersNode) { + return JSONUtils.parseObject(parametersNode.getTaskParams(), RemoteShellParameters.class); + } + + @Override + public ResourceParametersHelper getResources(String parameters) { + return JSONUtils.parseObject(parameters, RemoteShellParameters.class).getResources(); + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannelFactory.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannelFactory.java new file mode 100644 index 0000000000..394a0aae76 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/main/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskChannelFactory.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import org.apache.dolphinscheduler.plugin.task.api.TaskChannel; +import org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory; +import org.apache.dolphinscheduler.spi.params.base.ParamsOptions; +import org.apache.dolphinscheduler.spi.params.base.PluginParams; +import org.apache.dolphinscheduler.spi.params.base.Validate; +import org.apache.dolphinscheduler.spi.params.input.InputParam; +import org.apache.dolphinscheduler.spi.params.radio.RadioParam; + +import java.util.ArrayList; +import java.util.List; + +import com.google.auto.service.AutoService; + +@AutoService(TaskChannelFactory.class) +public class RemoteShellTaskChannelFactory implements TaskChannelFactory { + + @Override + public TaskChannel create() { + return new RemoteShellTaskChannel(); + } + + @Override + public String getName() { + return "REMOTESHELL"; + } + + @Override + public List getParams() { + List paramsList = new ArrayList<>(); + + InputParam nodeName = InputParam.newBuilder("name", "$t('Node name')") + .addValidate(Validate.newBuilder() + .setRequired(true) + .build()) + .build(); + + RadioParam runFlag = RadioParam.newBuilder("runFlag", "RUN_FLAG") + .addParamsOptions(new ParamsOptions("NORMAL", "NORMAL", false)) + .addParamsOptions(new ParamsOptions("FORBIDDEN", "FORBIDDEN", false)) + .build(); + + paramsList.add(nodeName); + paramsList.add(runFlag); + return paramsList; + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java new file mode 100644 index 0000000000..cd1687a17c --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteExecutorTest.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.plugin.datasource.ssh.SSHUtils; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceProcessor; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; + +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.session.ClientSession; + +import java.io.IOException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class RemoteExecutorTest { + + private String connectJson = + "{\"user\":\"root\",\"password\":\"123456\",\"host\":\"dolphinscheduler.com\",\"port\":22, \"publicKey\":\"ssh-rsa AAAAB\"}"; + + SSHConnectionParam sshConnectionParam; + + ClientSession clientSession; + + MockedStatic sshConnectionUtilsMockedStatic = org.mockito.Mockito.mockStatic(SSHUtils.class); + + @BeforeEach + void init() { + SSHDataSourceProcessor sshDataSourceProcessor = new SSHDataSourceProcessor(); + SSHDataSourceParamDTO sshDataSourceParamDTO = + (SSHDataSourceParamDTO) sshDataSourceProcessor.createDatasourceParamDTO(connectJson); + sshConnectionParam = sshDataSourceProcessor.createConnectionParams(sshDataSourceParamDTO); + clientSession = Mockito.mock(ClientSession.class, RETURNS_DEEP_STUBS); + sshConnectionUtilsMockedStatic.when(() -> SSHUtils.getSession(Mockito.any(), Mockito.any())) + .thenReturn(clientSession); + } + + @AfterEach + void tearDown() { + sshConnectionUtilsMockedStatic.close(); + } + + @Test + void testRunRemote() throws IOException { + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + ChannelExec channel = Mockito.mock(ChannelExec.class, RETURNS_DEEP_STUBS); + when(clientSession.auth().verify().isSuccess()).thenReturn(true); + when(clientSession.createExecChannel(Mockito.anyString())).thenReturn(channel); + when(channel.getExitStatus()).thenReturn(1); + Assertions.assertThrows(TaskException.class, () -> remoteExecutor.runRemote("ls -l")); + when(channel.getExitStatus()).thenReturn(0); + Assertions.assertDoesNotThrow(() -> remoteExecutor.runRemote("ls -l")); + } + + @Test + void testGetTaskPid() throws IOException { + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + String taskId = "1234"; + String command = String.format("ps -ef | grep \"%s.sh\" | grep -v grep | awk '{print $2}'", taskId); + doReturn("10001").when(remoteExecutor).runRemote(command); + Assertions.assertEquals("10001", remoteExecutor.getTaskPid(taskId)); + } + + @Test + void testSaveCommand() throws IOException { + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + doNothing().when(remoteExecutor).uploadScript(Mockito.anyString(), Mockito.anyString()); + String checkDirCommand = + "if [ ! -d /tmp/dolphinscheduler-remote-shell-root/ ]; then mkdir -p /tmp/dolphinscheduler-remote-shell-root/; fi"; + String catScriptCommand = "cat /tmp/dolphinscheduler-remote-shell-root/1234.sh"; + doReturn("").when(remoteExecutor).runRemote(checkDirCommand); + doReturn("").when(remoteExecutor).runRemote(catScriptCommand); + + remoteExecutor.saveCommand("1234", "/tmp/dolphinscheduler/test.sh"); + verify(remoteExecutor).runRemote(checkDirCommand); + } + + @Test + void testCleanData() throws IOException { + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + String cleanCommand = + "rm /tmp/dolphinscheduler-remote-shell-root/1234.sh /tmp/dolphinscheduler-remote-shell-root/1234.log"; + doReturn("").when(remoteExecutor).runRemote(cleanCommand); + remoteExecutor.cleanData("1234"); + String cleanCommandError = + "rm /tmp/dolphinscheduler-remote-shell-root/abcd.sh /tmp/dolphinscheduler-remote-shell-root/abcd.log"; + doThrow(new TaskException()).when(remoteExecutor).runRemote(cleanCommandError); + remoteExecutor.cleanData("abcd"); + } + + @Test + void testGetTaskExitCode() throws IOException { + RemoteExecutor remoteExecutor = spy(new RemoteExecutor(sshConnectionParam)); + String taskId = "1234"; + doNothing().when(remoteExecutor).cleanData(taskId); + String trackCommand = "tail -n 1 /tmp/dolphinscheduler-remote-shell-root/1234.log"; + doReturn("DOLPHINSCHEDULER-REMOTE-SHELL-TASK-STATUS-0").when(remoteExecutor).runRemote(trackCommand); + Assertions.assertEquals(0, remoteExecutor.getTaskExitCode(taskId)); + + doReturn("DOLPHINSCHEDULER-REMOTE-SHELL-TASK-STATUS-1").when(remoteExecutor).runRemote(trackCommand); + Assertions.assertEquals(1, remoteExecutor.getTaskExitCode(taskId)); + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskTest.java new file mode 100644 index 0000000000..2ecd9df98e --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-remoteshell/src/test/java/org/apache/dolphinscheduler/plugin/task/remoteshell/RemoteShellTaskTest.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.remoteshell; + +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; + +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHConnectionParam; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.ssh.param.SSHDataSourceProcessor; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; + +import org.apache.sshd.client.session.ClientSession; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class RemoteShellTaskTest { + + private String connectJson = + "{\"user\":\"root\",\"password\":\"123456\",\"host\":\"dolphinscheduler.com\",\"port\":22, \"publicKey\":\"ssh-rsa AAAAB\"}"; + + SSHConnectionParam sshConnectionParam; + + ClientSession clientSession; + + @BeforeEach + void init() { + SSHDataSourceProcessor sshDataSourceProcessor = new SSHDataSourceProcessor(); + SSHDataSourceParamDTO sshDataSourceParamDTO = + (SSHDataSourceParamDTO) sshDataSourceProcessor.createDatasourceParamDTO(connectJson); + sshConnectionParam = sshDataSourceProcessor.createConnectionParams(sshDataSourceParamDTO); + clientSession = Mockito.mock(ClientSession.class, RETURNS_DEEP_STUBS); + } + + @Test + void testBuildCommand() throws Exception { + TaskExecutionContext taskExecutionContext = new TaskExecutionContext(); + taskExecutionContext.setTaskAppId("1"); + taskExecutionContext + .setTaskParams("{\"localParams\":[],\"rawScript\":\"echo 1\",\"resourceList\":[],\"udfList\":[]}"); + taskExecutionContext.setExecutePath("/tmp"); + taskExecutionContext.setEnvironmentConfig("export PATH=/opt/anaconda3/bin:$PATH"); + RemoteShellTask remoteShellTask = spy(new RemoteShellTask(taskExecutionContext)); + doNothing().when(remoteShellTask).initRemoteExecutor(); + remoteShellTask.init(); + + MockedStatic filesMockedStatic = org.mockito.Mockito.mockStatic(Files.class); + filesMockedStatic.when(() -> Files.exists(Mockito.any())).thenReturn(false); + String script = "#!/bin/bash\n" + + "export PATH=/opt/anaconda3/bin:$PATH\n" + + "echo 1\n" + + "echo DOLPHINSCHEDULER-REMOTE-SHELL-TASK-STATUS-$?"; + Path path = Paths.get("/tmp/1_node.sh"); + filesMockedStatic.when(() -> Files.write(path, script.getBytes(), StandardOpenOption.APPEND)) + .thenThrow(new IOException("script match")); + + IOException exception = Assertions.assertThrows(IOException.class, () -> { + remoteShellTask.buildCommand(); + }); + Assertions.assertEquals("script match", exception.getMessage()); + } + +} diff --git a/dolphinscheduler-task-plugin/pom.xml b/dolphinscheduler-task-plugin/pom.xml index 27b741f06b..f4c1573226 100644 --- a/dolphinscheduler-task-plugin/pom.xml +++ b/dolphinscheduler-task-plugin/pom.xml @@ -62,6 +62,7 @@ dolphinscheduler-task-kubeflow dolphinscheduler-task-linkis dolphinscheduler-task-datafactory + dolphinscheduler-task-remoteshell diff --git a/dolphinscheduler-ui/public/images/task-icons/remoteshell.png b/dolphinscheduler-ui/public/images/task-icons/remoteshell.png new file mode 100644 index 0000000000000000000000000000000000000000..4e40b6eb2008b68e2c5766e054812a9993380132 GIT binary patch literal 747 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8p!3-p4i=A8uq!^2X+?^QKos)S9Wa|X@gt!6) z|AWE){rhLmoVjh=wnd8;ty;Be?%cUcmMj5^%%4Ah>C&Zh=F9=Ifl{+)&jyMB*$@#R z11K_U)+`_i6akVzjX*)52!snHA+m5~5Gf!D2N3l@F2pn-2?TIih&V(9lZ42kDTWKe z)#G8n^#awvl_8q|7xdc@^#B-N$t6L4!3>N{EUfI@JiL7T0)j#!V&W2V^2!>T+Ioh@ zCgxVwws!UoPA;xKzJ940S$X+I#ieBxmA#YZE?B&L)w=Z?H*Ma!bNAl;2M!-OcKpPd zvllL2y8htNlcz6Vy?y`T)*7s&PRaO{quBj46!(U_e!*JQ=rJPkIS1~ ziaZoAF{*NMF1+CASIHe1!uPS}|N94vwpU!<_U?E5yPs!g*;Zf6NqBKvW17fQr&!@7 zyQVq2b3Xp**k!mN<3wQERUL)f zyzzSuEty!nt>(~C-SirMFaLrQWfBK+wAPCM?OyZtJO@M0*J)2KHGGp`->{2=VZ(8u zx!a!qQhU21O!d0+Zk^xH*|+_3+us-V@8-Sho$oH3)%Fcc%>A){S-x9$+bn*c6dPbj OFnGH9xvXdxEk6 literal 0 HcmV?d00001 diff --git a/dolphinscheduler-ui/public/images/task-icons/remoteshell_hover.png b/dolphinscheduler-ui/public/images/task-icons/remoteshell_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..b615f5532e35bf3a98783678f64f28af4d2720d7 GIT binary patch literal 745 zcmeAS@N?(olHy`uVBq!ia0vp^Y9P$P3?%12mYf5m7>k44ofy`glX(eb>je0OxB>(&i5T?%*PZrXdk%=LH~qiP1dy!Og#VgT z{_9NouRHm_PXB)`AR8z?>A%Lr|2mUFvN{w0YXcdR{%cP7uQ3Uv2FTTfa3}oNp7dWE zC=S#z5u^ZQ+Qk1FKy!c$2pcF1afmFK1nLDU04W9{Afq3|1qwn`BBa2! zz-19?VCoSfU;`1xf(-|8k*r(!rJxoVUdbgve!&ckOf0PI+&sK|`~rePB4Xkaa`MU= zn%a7X#wO-g*0y%`4o)tvKE8gb8CiMxMa89M6_ve{<}O&geAT-38#isojdhY+t+M%5r-SS>O9DHTnlS8Yg-jF61)W9g_5}D>Ci$g}L3X zzW!Uo4_V#bV{rfR>jl#@*=@d7C-A1*u)Os-D}LwujhRvFcf8zWyfwb~Wh3jRJJ%v2 z|5~0`e7$9tbCL5k?JMs-3vRSO!S>VB^X|vD`_oPt*8j`)nj|(y)avK)uud;2`3Wkf z=Z@AzztNq!EhTD6ieR*w^gHd_U#t_`dqZ!0+&=Bli=UT!7G80g`Zvd(Z(rN2)gPuS Q1H*yA)78&qol`;+08kKhUjP6A literal 0 HcmV?d00001 diff --git a/dolphinscheduler-ui/src/service/modules/data-source/types.ts b/dolphinscheduler-ui/src/service/modules/data-source/types.ts index 4da8d63724..10a200c9db 100644 --- a/dolphinscheduler-ui/src/service/modules/data-source/types.ts +++ b/dolphinscheduler-ui/src/service/modules/data-source/types.ts @@ -32,6 +32,7 @@ type IDataBase = | 'STARROCKS' | 'DAMENG' | 'OCEANBASE' + | 'SSH' type IDataBaseLabel = | 'MYSQL' @@ -50,6 +51,7 @@ type IDataBaseLabel = | 'STARROCKS' | 'DAMENG' | 'OCEANBASE' +| 'SSH' interface IDataSource { id?: number @@ -76,6 +78,7 @@ interface IDataSource { MSIClientId?: string dbUser?: string compatibleMode?: string + publicKey?: string } interface ListReq { diff --git a/dolphinscheduler-ui/src/store/project/task-type.ts b/dolphinscheduler-ui/src/store/project/task-type.ts index 24bc05614d..993cf5a1f0 100644 --- a/dolphinscheduler-ui/src/store/project/task-type.ts +++ b/dolphinscheduler-ui/src/store/project/task-type.ts @@ -153,6 +153,10 @@ export const TASK_TYPES_MAP = { DATA_FACTORY: { alias: 'DATA_FACTORY', helperLinkDisable: true + }, + REMOTESHELL: { + alias: 'REMOTESHELL', + helperLinkDisable: true } } as { [key in TaskType]: { diff --git a/dolphinscheduler-ui/src/store/project/types.ts b/dolphinscheduler-ui/src/store/project/types.ts index 0df130e77b..7c2136f08c 100644 --- a/dolphinscheduler-ui/src/store/project/types.ts +++ b/dolphinscheduler-ui/src/store/project/types.ts @@ -57,6 +57,7 @@ type TaskType = | 'KUBEFLOW' | 'LINKIS' | 'DATA_FACTORY' + | 'REMOTESHELL' type ProgramType = 'JAVA' | 'SCALA' | 'PYTHON' diff --git a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx index cef9bab214..8b5d6d2cf5 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx +++ b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx @@ -162,6 +162,9 @@ const DetailModal = defineComponent({ showConnectType, showPrincipal, showMode, + showDataBaseName, + showJDBCConnectParameters, + showPublicKey, modeOptions, redShitModeOptions, loading, @@ -539,6 +542,7 @@ const DetailModal = defineComponent({ /> @@ -634,6 +639,19 @@ const DetailModal = defineComponent({ options={this.bindTestDataSourceExample} /> + + + ), diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts index 94a0549b64..d1181cb03f 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts @@ -69,6 +69,9 @@ export function useForm(id?: number) { showConnectType: false, showPrincipal: false, showMode: false, + showDataBaseName: true, + showJDBCConnectParameters: true, + showPublicKey: false, bindTestDataSourceExample: [] as { label: string; value: number }[], rules: { name: { @@ -263,6 +266,19 @@ export function useForm(id?: number) { } else { state.showPrincipal = false } + if (type === 'SSH') { + state.showDataBaseName = false + state.requiredDataBase = false + state.showJDBCConnectParameters = false + state.showPublicKey = true + }else { + state.showDataBaseName = true + state.requiredDataBase = true + state.showJDBCConnectParameters = true + state.showPublicKey = false + + } + if (state.detailForm.id === undefined) { await getSameTypeTestDataSource() } @@ -406,6 +422,11 @@ export const datasourceType: IDataBaseOptionKeys = { value: 'OCEANBASE', label: 'OCEANBASE', defaultPort: 2881 + }, + SSH: { + value: 'SSH', + label: 'SSH', + defaultPort: 22 } } @@ -414,4 +435,4 @@ export const datasourceTypeList: IDataBaseOption[] = Object.values( ).map((item) => { item.class = 'options-datasource-type' return item -}) \ No newline at end of file +}) diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts index 4510f47183..d8177f6410 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts @@ -86,3 +86,4 @@ export { useDatasync } from './use-datasync' export { useKubeflow } from './use-kubeflow' export { useLinkis } from './use-linkis' export { useDataFactory } from './use-data-factory' +export { useRemoteShell } from './use-remote-shell' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index 984ad08b69..4f9f132947 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -112,7 +112,12 @@ export function useDatasource( id: 15, code: 'DAMENG', disabled: false - } + }, + { + id: 15, + code: 'SSH', + disabled: true + }, ] const getDatasourceTypes = async () => { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-remote-shell.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-remote-shell.ts new file mode 100644 index 0000000000..a3a6f55e46 --- /dev/null +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-remote-shell.ts @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useI18n } from 'vue-i18n' +import { useCustomParams } from '.' +import type { IJsonItem } from '../types' + +export function useRemoteShell(model: { [field: string]: any }): IJsonItem[] { + const { t } = useI18n() + + return [ + { + type: 'editor', + field: 'rawScript', + name: t('project.node.script'), + validate: { + trigger: ['input', 'trigger'], + required: true, + message: t('project.node.script_tips') + } + }, + ...useCustomParams({ model, field: 'localParams', isSimple: false }) + ] +} diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts index 75e69079df..b4c346834a 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts @@ -465,6 +465,11 @@ export function formatParams(data: INodeData): { taskParams.pipelineName = data.pipelineName } + if (data.taskType === 'REMOTESHELL') { + taskParams.type = data.type + taskParams.datasource = data.datasource + } + let timeoutNotifyStrategy = '' if (data.timeoutNotifyStrategy) { if (data.timeoutNotifyStrategy.length === 1) { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts index 04a3518242..0372421357 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts @@ -51,6 +51,7 @@ import { useDatasync } from './use-datasync' import { useKubeflow } from './use-kubeflow' import { useLinkis } from './use-linkis' import { useDataFactory } from './use-data-factory' +import { useRemoteShell } from './use-remote-shell' export default { SHELL: useShell, @@ -88,5 +89,6 @@ export default { DATASYNC: useDatasync, KUBEFLOW: useKubeflow, LINKIS: useLinkis, - DATA_FACTORY: useDataFactory + DATA_FACTORY: useDataFactory, + REMOTESHELL: useRemoteShell } diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-remote-shell.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-remote-shell.ts new file mode 100644 index 0000000000..5e31de6da8 --- /dev/null +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-remote-shell.ts @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { reactive } from 'vue' +import * as Fields from '../fields/index' +import type { IJsonItem, INodeData, ITaskData } from '../types' + +export function useRemoteShell({ + projectCode, + from = 0, + readonly, + data +}: { + projectCode: number + from?: number + readonly?: boolean + data?: ITaskData +}) { + const model = reactive({ + name: '', + taskType: 'REMOTESHELL', + flag: 'YES', + description: '', + timeoutFlag: false, + timeoutNotifyStrategy: ['WARN'], + timeout: 30, + localParams: [], + environmentCode: null, + failRetryInterval: 1, + failRetryTimes: 0, + workerGroup: 'default', + delayTime: 0, + type: 'SSH', + rawScript: '' + } as INodeData) + + return { + json: [ + Fields.useName(from), + ...Fields.useTaskDefinition({ projectCode, from, readonly, data, model }), + Fields.useRunFlag(), + Fields.useCache(), + Fields.useDescription(), + Fields.useTaskPriority(), + Fields.useWorkerGroup(), + Fields.useEnvironmentName(model, !data?.id), + ...Fields.useTaskGroup(model, projectCode), + ...Fields.useFailed(), + ...Fields.useResourceLimit(), + Fields.useDelayTime(model), + ...Fields.useTimeoutAlarm(model), + ...Fields.useDatasource(model, { + supportedDatasourceType: ['SSH'] + }), + ...Fields.useRemoteShell(model), + Fields.usePreTasks() + ] as IJsonItem[], + model + } +} diff --git a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts index 738892f68b..03d523cdaf 100644 --- a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts +++ b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts @@ -51,6 +51,7 @@ export type TaskType = | 'KUBEFLOW' | 'LINKIS' | 'DATA_FACTORY' + | 'REMOTESHELL' export type TaskExecuteType = 'STREAM' | 'BATCH' @@ -185,6 +186,10 @@ export const TASK_TYPES_MAP = { DATA_FACTORY: { alias: 'DATA_FACTORY', helperLinkDisable: true + }, + REMOTESHELL: { + alias: 'REMOTESHELL', + helperLinkDisable: true } } as { [key in TaskType]: { diff --git a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss index 65b7aa0067..b9a52da161 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss +++ b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss @@ -207,6 +207,9 @@ $bgLight: #ffffff; &.icon-data_factory { background-image: url('/images/task-icons/data_factory.png'); } + &.icon-remoteshell { + background-image: url('/images/task-icons/remoteshell.png'); + } } &:hover { @@ -317,6 +320,9 @@ $bgLight: #ffffff; &.icon-data_factory { background-image: url('/images/task-icons/data_factory_hover.png'); } + &.icon-remoteshell { + background-image: url('/images/task-icons/remoteshell_hover.png'); + } } } diff --git a/tools/dependencies/known-dependencies.txt b/tools/dependencies/known-dependencies.txt index ef1a804460..b310820234 100644 --- a/tools/dependencies/known-dependencies.txt +++ b/tools/dependencies/known-dependencies.txt @@ -468,4 +468,9 @@ opencensus-proto-0.2.0.jar proto-google-cloud-storage-v2-2.18.0-alpha.jar proto-google-iam-v1-1.9.0.jar re2j-1.6.jar -threetenbp-1.6.5.jar \ No newline at end of file +threetenbp-1.6.5.jar +sshd-scp-2.8.0.jar +sshd-sftp-2.8.0.jar +sshd-common-2.8.0.jar +sshd-core-2.8.0.jar +jcl-over-slf4j-1.7.36.jar