mirror of https://github.com/jumpserver/jumpserver
Merge branch 'pam' of github.com:jumpserver/jumpserver into pam
commit
53fa5a2320
|
@ -20,6 +20,7 @@
|
||||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -39,7 +40,10 @@
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
commands: "{{ params.commands }}"
|
commands: "{{ params.commands }}"
|
||||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
answers: "{{ params.answers }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
delay_time: "{{ params.delay_time | default(2) }}"
|
||||||
|
prompt: "{{ params.prompt | default('.*') }}"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: ping_info is succeeded and check_conn_after_change
|
when: ping_info is succeeded and check_conn_after_change
|
||||||
register: change_info
|
register: change_info
|
||||||
|
@ -58,5 +62,6 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
when: check_conn_after_change
|
when: check_conn_after_change
|
|
@ -10,10 +10,30 @@ protocol: ssh
|
||||||
priority: 50
|
priority: 50
|
||||||
params:
|
params:
|
||||||
- name: commands
|
- name: commands
|
||||||
type: list
|
type: text
|
||||||
label: "{{ 'Params commands label' | trans }}"
|
label: "{{ 'Params commands label' | trans }}"
|
||||||
default: [ '' ]
|
default: ''
|
||||||
help_text: "{{ 'Params commands help text' | trans }}"
|
help_text: "{{ 'Params commands help text' | trans }}"
|
||||||
|
- name: recv_timeout
|
||||||
|
type: int
|
||||||
|
label: "{{ 'Params recv_timeout label' | trans }}"
|
||||||
|
default: 30
|
||||||
|
help_text: "{{ 'Params recv_timeout help text' | trans }}"
|
||||||
|
- name: delay_time
|
||||||
|
type: int
|
||||||
|
label: "{{ 'Params delay_time label' | trans }}"
|
||||||
|
default: 2
|
||||||
|
help_text: "{{ 'Params delay_time help text' | trans }}"
|
||||||
|
- name: prompt
|
||||||
|
type: str
|
||||||
|
label: "{{ 'Params prompt label' | trans }}"
|
||||||
|
default: '.*'
|
||||||
|
help_text: "{{ 'Params prompt help text' | trans }}"
|
||||||
|
- name: answers
|
||||||
|
type: text
|
||||||
|
label: "{{ 'Params answer label' | trans }}"
|
||||||
|
default: '.*'
|
||||||
|
help_text: "{{ 'Params answer help text' | trans }}"
|
||||||
|
|
||||||
i18n:
|
i18n:
|
||||||
SSH account change secret:
|
SSH account change secret:
|
||||||
|
@ -22,11 +42,91 @@ i18n:
|
||||||
en: 'Custom password change by SSH command line'
|
en: 'Custom password change by SSH command line'
|
||||||
|
|
||||||
Params commands help text:
|
Params commands help text:
|
||||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
zh: |
|
||||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
请将命令中的指定位置改成特殊符号 <br />
|
||||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
1. 改密账号 -> {username} <br />
|
||||||
|
2. 改密密码 -> {password} <br />
|
||||||
|
3. 登录用户密码 -> {login_password} <br />
|
||||||
|
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
|
||||||
|
比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
ja: |
|
||||||
|
コマンド内の指定された位置を特殊記号に変更してください。<br />
|
||||||
|
新しいパスワード(アカウント変更) -> {username} <br />
|
||||||
|
新しいパスワード(パスワード変更) -> {password} <br />
|
||||||
|
ログインユーザーパスワード -> {login_password} <br />
|
||||||
|
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
|
||||||
|
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
en: |
|
||||||
|
Please change the specified positions in the command to special symbols. <br />
|
||||||
|
Change password account -> {username} <br />
|
||||||
|
Change password -> {password} <br />
|
||||||
|
Login user password -> {login_password} <br />
|
||||||
|
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
|
||||||
|
the system will replace the special symbols with real data. <br />
|
||||||
|
For example, to change the password for a Cisco device, you generally need to configure five commands: <br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
|
||||||
Params commands label:
|
Params commands label:
|
||||||
zh: '自定义命令'
|
zh: '自定义命令'
|
||||||
ja: 'カスタムコマンド'
|
ja: 'カスタムコマンド'
|
||||||
en: 'Custom command'
|
en: 'Custom command'
|
||||||
|
|
||||||
|
Params recv_timeout label:
|
||||||
|
zh: '超时时间'
|
||||||
|
ja: 'タイムアウト'
|
||||||
|
en: 'Timeout'
|
||||||
|
|
||||||
|
Params recv_timeout help text:
|
||||||
|
zh: '等待命令结果返回的超时时间(秒)'
|
||||||
|
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
|
||||||
|
en: 'The timeout for waiting for the command result to return (Seconds)'
|
||||||
|
|
||||||
|
Params delay_time label:
|
||||||
|
zh: '延迟发送时间'
|
||||||
|
ja: '遅延送信時間'
|
||||||
|
en: 'Delayed send time'
|
||||||
|
|
||||||
|
Params delay_time help text:
|
||||||
|
zh: '每条命令延迟发送的时间间隔(秒)'
|
||||||
|
ja: '各コマンド送信の遅延間隔(秒)'
|
||||||
|
en: 'Time interval for each command delay in sending (Seconds)'
|
||||||
|
|
||||||
|
Params prompt label:
|
||||||
|
zh: '提示符'
|
||||||
|
ja: 'ヒント'
|
||||||
|
en: 'Prompt'
|
||||||
|
|
||||||
|
Params prompt help text:
|
||||||
|
zh: '终端连接后显示的提示符信息(正则表达式)'
|
||||||
|
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
|
||||||
|
en: 'Prompt information displayed after terminal connection (Regular expression)'
|
||||||
|
|
||||||
|
Params answer label:
|
||||||
|
zh: '命令结果'
|
||||||
|
ja: 'コマンド結果'
|
||||||
|
en: 'Command result'
|
||||||
|
|
||||||
|
Params answer help text:
|
||||||
|
zh: |
|
||||||
|
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
|
||||||
|
ja: |
|
||||||
|
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
|
||||||
|
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
|
||||||
|
en: |
|
||||||
|
Decide whether to execute the next command based on the result match.
|
||||||
|
The input content corresponds line by line with the content
|
||||||
|
of the `Custom command` above. (Regular expression)
|
||||||
|
|
|
@ -21,3 +21,4 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
|
|
@ -1,133 +1,121 @@
|
||||||
# JumpServer PAM 客户端
|
|
||||||
|
|
||||||
该包提供了一个 Go 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- 在发送请求之前验证参数。
|
|
||||||
- 支持基于资产和账户的密码检索。
|
|
||||||
- 使用 HMAC-SHA256 签名进行身份验证,方便与 JumpServer PAM API 集成。
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
1. **下载 Go 代码文件**:
|
|
||||||
将代码文件下载到您的项目目录中。
|
|
||||||
|
|
||||||
2. **导入包**:
|
|
||||||
在您的 Go 文件中导入该包,您即可直接使用其功能。
|
|
||||||
|
|
||||||
## 需求
|
|
||||||
|
|
||||||
- `Go 1.16+`
|
|
||||||
- `github.com/google/uuid`
|
|
||||||
- `gopkg.in/twindagger/httpsig.v1`
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 初始化
|
|
||||||
|
|
||||||
要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`keyID` 和 `keySecret` 创建一个实例。
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"your_module_path/jms_pam"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
type APIClient struct {
|
||||||
client := jms_pam.NewJumpServerPAM(
|
Client *http.Client
|
||||||
"http://127.0.0.1", // 替换为您的 JumpServer 端点
|
APIURL string
|
||||||
"your-key-id", // 替换为您的实际 Key ID
|
KeyID string
|
||||||
"your-key-secret", // 替换为您的实际 Key Secret
|
KeySecret string
|
||||||
"", // 留空以使用默认的组织 ID
|
OrgID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIClient() *APIClient {
|
||||||
|
return &APIClient{
|
||||||
|
Client: &http.Client{},
|
||||||
|
APIURL: getEnv("API_URL", "http://127.0.0.1:8080"),
|
||||||
|
KeyID: getEnv("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e"),
|
||||||
|
KeySecret: getEnv("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8"),
|
||||||
|
OrgID: getEnv("ORG_ID", "00000000-0000-0000-0000-000000000002"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) GetAccountSecret(asset, account string) (map[string]interface{}, error) {
|
||||||
|
u, err := url.Parse(c.APIURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse API URL: %v", err)
|
||||||
|
}
|
||||||
|
u.Path = "/api/v1/accounts/integration-applications/account-secret/"
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
q.Add("asset", asset)
|
||||||
|
q.Add("account", account)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-JMS-ORG", c.OrgID)
|
||||||
|
req.Header.Set("Date", date)
|
||||||
|
req.Header.Set("X-Source", "jms-pam")
|
||||||
|
|
||||||
|
headersList := []string{"(request-target)", "accept", "date", "x-jms-org"}
|
||||||
|
var signatureParts []string
|
||||||
|
|
||||||
|
for _, h := range headersList {
|
||||||
|
var value string
|
||||||
|
if h == "(request-target)" {
|
||||||
|
value = strings.ToLower(req.Method) + " " + req.URL.RequestURI()
|
||||||
|
} else {
|
||||||
|
canonicalKey := http.CanonicalHeaderKey(h)
|
||||||
|
value = req.Header.Get(canonicalKey)
|
||||||
|
}
|
||||||
|
signatureParts = append(signatureParts, fmt.Sprintf("%s: %s", h, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureString := strings.Join(signatureParts, "\n")
|
||||||
|
mac := hmac.New(sha256.New, []byte(c.KeySecret))
|
||||||
|
mac.Write([]byte(signatureString))
|
||||||
|
signatureB64 := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
headersJoined := strings.Join(headersList, " ")
|
||||||
|
authHeader := fmt.Sprintf(
|
||||||
|
`Signature keyId="%s",algorithm="hmac-sha256",headers="%s",signature="%s"`,
|
||||||
|
c.KeyID,
|
||||||
|
headersJoined,
|
||||||
|
signatureB64,
|
||||||
)
|
)
|
||||||
|
req.Header.Set("Authorization", authHeader)
|
||||||
|
|
||||||
|
resp, err := c.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API returned non-200 status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
### 创建密码请求
|
|
||||||
|
|
||||||
您可以通过指定资产或账户信息来创建请求。
|
|
||||||
|
|
||||||
```go
|
|
||||||
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("创建请求时出错:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 发送请求
|
|
||||||
|
|
||||||
使用客户端的 `Send` 方法发送请求。
|
|
||||||
|
|
||||||
```go
|
|
||||||
secretObj, err := client.Send(request)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("发送请求时出错:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 处理响应
|
|
||||||
|
|
||||||
检查密码是否成功检索,并相应地处理响应。
|
|
||||||
|
|
||||||
```go
|
|
||||||
if secretObj.Valid {
|
|
||||||
fmt.Println("密码:", secretObj.Secret)
|
|
||||||
} else {
|
|
||||||
fmt.Println("获取密码失败:", string(secretObj.Desc))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 完整示例
|
|
||||||
|
|
||||||
以下是如何使用该客户端的完整示例:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"your_module_path/jms_pam"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
client := jms_pam.NewJumpServerPAM(
|
client := NewAPIClient()
|
||||||
"http://127.0.0.1",
|
result, err := client.GetAccountSecret("ubuntu_docker", "root")
|
||||||
"your-key-id",
|
|
||||||
"your-key-secret",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
request, err := jms_pam.NewSecretRequest("Linux", "", "root", "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("创建请求时出错:", err)
|
log.Fatalf("Error: %v", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
secretObj, err := client.Send(request)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("发送请求时出错:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if secretObj.Valid {
|
|
||||||
fmt.Println("密码:", secretObj.Secret)
|
|
||||||
} else {
|
|
||||||
fmt.Println("获取密码失败:", string(secretObj.Desc))
|
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Result: %+v\n", result)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
该库会在创建 `SecretRequest` 时返回无效参数的错误。这包括对有效 UUID 的检查以及确保提供了必需的参数。
|
|
||||||
|
|
||||||
## 贡献
|
|
||||||
|
|
||||||
欢迎贡献!如有任何增强或错误修复,请提出问题或提交拉取请求。
|
|
|
@ -1,96 +1,45 @@
|
||||||
# JumpServer PAM 客户端
|
|
||||||
|
|
||||||
该包提供了一个 Python 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
|
|
||||||
- 在发送请求之前验证参数。
|
|
||||||
- 支持基于资产和账户的密码检索。
|
|
||||||
- 通过 HTTP 签名轻松集成 JumpServer PAM API。
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
您可以通过 pip 安装该包:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install jms_pam-0.0.1-py3-none-any.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
## 需求
|
|
||||||
|
|
||||||
- `Python 3.6+`
|
|
||||||
- `requests`
|
|
||||||
- `httpsig`
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 初始化
|
|
||||||
|
|
||||||
要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`key_id` 和 `key_secret` 创建一个实例。
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from jms_pam import JumpServerPAM, SecretRequest
|
import requests
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from httpsig.requests_auth import HTTPSignatureAuth
|
||||||
|
|
||||||
client = JumpServerPAM(
|
API_URL = os.getenv("API_URL", "http://127.0.0.1:8080")
|
||||||
endpoint='http://127.0.0.1',
|
KEY_ID = os.getenv("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e")
|
||||||
key_id='your-key-id',
|
KEY_SECRET = os.getenv("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8")
|
||||||
key_secret='your-key-secret'
|
ORG_ID = os.getenv("ORG_ID", "00000000-0000-0000-0000-000000000002")
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 创建密码请求
|
|
||||||
|
|
||||||
您可以通过指定资产或账户信息来创建一个密码请求。
|
class APIClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.auth = HTTPSignatureAuth(
|
||||||
|
key_id=KEY_ID, secret=KEY_SECRET,
|
||||||
|
algorithm='hmac-sha256', headers=['(request-target)', 'accept', 'date', 'x-jms-org']
|
||||||
|
)
|
||||||
|
|
||||||
```python
|
def get_account_secret(self, asset, account):
|
||||||
request = SecretRequest(asset='Linux', account='root')
|
url = f"{API_URL}/api/v1/accounts/integration-applications/account-secret/"
|
||||||
```
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-JMS-ORG': ORG_ID,
|
||||||
|
'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
||||||
|
'X-Source': 'jms-pam'
|
||||||
|
}
|
||||||
|
params = {"asset": asset, "account": account}
|
||||||
|
|
||||||
### 发送请求
|
try:
|
||||||
|
response = self.session.get(url, auth=self.auth, headers=headers, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
print(f"API 请求失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
使用客户端的 `send` 方法发送请求。
|
|
||||||
|
|
||||||
```python
|
# 示例调用
|
||||||
secret_obj = client.send(request)
|
if __name__ == "__main__":
|
||||||
```
|
client = APIClient()
|
||||||
|
result = client.get_account_secret(asset="ubuntu_docker", account="root")
|
||||||
### 处理响应
|
print(result)
|
||||||
|
```
|
||||||
检查密码是否成功检索,并相应地处理响应。
|
|
||||||
|
|
||||||
```python
|
|
||||||
if secret_obj.valid:
|
|
||||||
print('密码: %s' % secret_obj.secret)
|
|
||||||
else:
|
|
||||||
print('获取密码失败: %s' % secret_obj.desc)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 完整示例
|
|
||||||
|
|
||||||
以下是如何使用该客户端的完整示例:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from jms_pam import JumpServerPAM, SecretRequest
|
|
||||||
|
|
||||||
client = JumpServerPAM(
|
|
||||||
endpoint='http://127.0.0.1',
|
|
||||||
key_id='your-key-id',
|
|
||||||
key_secret='your-key-secret'
|
|
||||||
)
|
|
||||||
|
|
||||||
request = SecretRequest(asset='Linux', account='root')
|
|
||||||
secret_obj = client.send(request)
|
|
||||||
|
|
||||||
if secret_obj.valid:
|
|
||||||
print('密码: %s' % secret_obj.secret)
|
|
||||||
else:
|
|
||||||
print('获取密码失败: %s' % secret_obj.desc)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
如果提供的参数不符合验证要求,库会引发 `RequestParamsError`。这包括对有效 UUID 的检查和参数之间的相互依赖性检查。
|
|
||||||
|
|
||||||
## 贡献
|
|
||||||
|
|
||||||
欢迎贡献!请打开一个问题或提交拉取请求,以进行任何增强或修复错误。
|
|
|
@ -21,4 +21,5 @@
|
||||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
|
||||||
|
|
|
@ -39,8 +39,7 @@ class DeviceTypes(BaseType):
|
||||||
'*': {
|
'*': {
|
||||||
'ansible_enabled': True,
|
'ansible_enabled': True,
|
||||||
'ansible_config': {
|
'ansible_config': {
|
||||||
'ansible_connection': 'local',
|
'ansible_connection': 'local'
|
||||||
'first_conn_delay_time': 0.5,
|
|
||||||
},
|
},
|
||||||
'ping_enabled': True,
|
'ping_enabled': True,
|
||||||
'gather_facts_enabled': False,
|
'gather_facts_enabled': False,
|
||||||
|
|
|
@ -33,13 +33,8 @@ options:
|
||||||
commands:
|
commands:
|
||||||
description:
|
description:
|
||||||
- Custom change password commands.
|
- Custom change password commands.
|
||||||
type: list
|
type: str
|
||||||
required: true
|
required: true
|
||||||
first_conn_delay_time:
|
|
||||||
description:
|
|
||||||
- Delay for executing the command after SSH connection(unit: s)
|
|
||||||
type: float
|
|
||||||
required: false
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
|
@ -51,7 +46,7 @@ EXAMPLES = '''
|
||||||
login_password: "123456"
|
login_password: "123456"
|
||||||
name: "jms"
|
name: "jms"
|
||||||
password: "123456"
|
password: "123456"
|
||||||
commands: ['passwd {username}', '{password}', '{password}']
|
commands: 'passwd {username}\n{password}\n{password}']
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = '''
|
RETURN = '''
|
||||||
|
@ -63,21 +58,25 @@ name:
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
from libs.ansible.modules_utils.custom_common import (
|
from libs.ansible.modules_utils.remote_client import (
|
||||||
SSHClient, common_argument_spec
|
SSHClient, common_argument_spec
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_commands(module):
|
def get_commands_and_answers(module) -> (list, list):
|
||||||
username = module.params['name']
|
username = module.params['name']
|
||||||
password = module.params['password']
|
password = module.params['password']
|
||||||
commands = module.params['commands'] or []
|
commands = module.params['commands'] or ''
|
||||||
|
answers = module.params['answers'] or ''
|
||||||
login_password = module.params['login_password']
|
login_password = module.params['login_password']
|
||||||
for index, command in enumerate(commands):
|
|
||||||
commands[index] = command.format(
|
if isinstance(commands, list):
|
||||||
username=username, password=password, login_password=login_password
|
commands = '\n'.join(commands)
|
||||||
)
|
commands = commands.format(
|
||||||
return commands
|
username=username, password=password, login_password=login_password
|
||||||
|
)
|
||||||
|
return commands.split('\n'), answers.split('\n')
|
||||||
|
|
||||||
|
|
||||||
# =========================================
|
# =========================================
|
||||||
# Module execution.
|
# Module execution.
|
||||||
|
@ -89,21 +88,20 @@ def main():
|
||||||
argument_spec.update(
|
argument_spec.update(
|
||||||
name=dict(required=True, aliases=['user']),
|
name=dict(required=True, aliases=['user']),
|
||||||
password=dict(aliases=['pass'], no_log=True),
|
password=dict(aliases=['pass'], no_log=True),
|
||||||
commands=dict(type='list', required=False),
|
|
||||||
)
|
)
|
||||||
module = AnsibleModule(argument_spec=argument_spec)
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
ssh_client = SSHClient(module)
|
commands, answers = get_commands_and_answers(module)
|
||||||
commands = get_commands(module)
|
|
||||||
if not commands:
|
if not commands:
|
||||||
module.fail_json(
|
module.fail_json(
|
||||||
msg='No command found, please go to the platform details to add'
|
msg='No command found, please go to the platform details to add'
|
||||||
)
|
)
|
||||||
output, err_msg = ssh_client.execute(commands)
|
with SSHClient(module) as client:
|
||||||
if err_msg:
|
__, err_msg = client.execute(commands, answers)
|
||||||
module.fail_json(
|
if err_msg:
|
||||||
msg='There was a problem executing the command: %s' % err_msg
|
module.fail_json(
|
||||||
)
|
msg='There was a problem executing the command: %s' % err_msg
|
||||||
|
)
|
||||||
|
|
||||||
user = module.params['name']
|
user = module.params['name']
|
||||||
module.exit_json(changed=True, user=user)
|
module.exit_json(changed=True, user=user)
|
||||||
|
|
|
@ -116,7 +116,7 @@ def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.close()
|
client.close()
|
||||||
except Exception:
|
except Exception: # noqa
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return module.exit_json(**result)
|
return module.exit_json(**result)
|
||||||
|
|
|
@ -7,14 +7,44 @@ __metaclass__ = type
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
module: custom_rdp_ping
|
module: custom_rdp_ping
|
||||||
short_description: Use rdp to probe whether an asset is connectable
|
short_description: Use RDP to probe whether an asset is connectable.
|
||||||
description:
|
description:
|
||||||
- Use rdp to probe whether an asset is connectable
|
- Use RDP to probe whether an asset is connectable.
|
||||||
|
options:
|
||||||
|
login_host:
|
||||||
|
description: Target host to connect.
|
||||||
|
type: str
|
||||||
|
required: False
|
||||||
|
default: localhost
|
||||||
|
login_port:
|
||||||
|
description: Target port to connect.
|
||||||
|
type: int
|
||||||
|
required: False
|
||||||
|
default: 22
|
||||||
|
login_user:
|
||||||
|
description: Login user for the connection.
|
||||||
|
type: str
|
||||||
|
required: False
|
||||||
|
default: root
|
||||||
|
login_password:
|
||||||
|
description: Login password.
|
||||||
|
type: str
|
||||||
|
required: False
|
||||||
|
no_log: True
|
||||||
|
login_secret_type:
|
||||||
|
description: Authentication method.
|
||||||
|
type: str
|
||||||
|
required: False
|
||||||
|
default: password
|
||||||
|
gateway_args:
|
||||||
|
description: Arguments for setting up an SSH tunnel.
|
||||||
|
type: dict
|
||||||
|
required: False
|
||||||
|
default: null
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
- name: >
|
- name: Ping asset server using RDP.
|
||||||
Ping asset server.
|
|
||||||
custom_rdp_ping:
|
custom_rdp_ping:
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: 3389
|
login_port: 3389
|
||||||
|
@ -24,12 +54,12 @@ EXAMPLES = '''
|
||||||
|
|
||||||
RETURN = '''
|
RETURN = '''
|
||||||
is_available:
|
is_available:
|
||||||
description: Windows server availability.
|
description: Indicates if the Windows asset is available.
|
||||||
returned: always
|
returned: always
|
||||||
type: bool
|
type: bool
|
||||||
sample: true
|
sample: true
|
||||||
conn_err_msg:
|
conn_err_msg:
|
||||||
description: Connection error message.
|
description: Connection error message (if any).
|
||||||
returned: always
|
returned: always
|
||||||
type: str
|
type: str
|
||||||
sample: ''
|
sample: ''
|
||||||
|
@ -41,11 +71,6 @@ from sshtunnel import SSHTunnelForwarder
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
# =========================================
|
|
||||||
# Module execution.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
def common_argument_spec():
|
def common_argument_spec():
|
||||||
options = dict(
|
options = dict(
|
||||||
login_host=dict(type='str', required=False, default='localhost'),
|
login_host=dict(type='str', required=False, default='localhost'),
|
||||||
|
@ -67,13 +92,12 @@ class RDPConnectionManager:
|
||||||
self.result_queue = multiprocessing.Queue()
|
self.result_queue = multiprocessing.Queue()
|
||||||
|
|
||||||
def build_connection_details(self):
|
def build_connection_details(self):
|
||||||
connection_details = {
|
return {
|
||||||
'hostname': self.params['login_host'],
|
'hostname': self.params['login_host'],
|
||||||
'port': self.params['login_port'],
|
'port': self.params['login_port'],
|
||||||
'username': self.params['username'],
|
'username': self.params['login_user'],
|
||||||
'password': self.params['password']
|
'password': self.params['login_password']
|
||||||
}
|
}
|
||||||
return connection_details
|
|
||||||
|
|
||||||
def setup_ssh_tunnel(self):
|
def setup_ssh_tunnel(self):
|
||||||
gateway_args = self.params['gateway_args'] or {}
|
gateway_args = self.params['gateway_args'] or {}
|
||||||
|
@ -90,8 +114,8 @@ class RDPConnectionManager:
|
||||||
self.connection_details['port']
|
self.connection_details['port']
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
tunnel.start()
|
tunnel.start()
|
||||||
|
|
||||||
self.connection_details['hostname'] = '127.0.0.1'
|
self.connection_details['hostname'] = '127.0.0.1'
|
||||||
self.connection_details['port'] = tunnel.local_bind_port
|
self.connection_details['port'] = tunnel.local_bind_port
|
||||||
self.ssh_tunnel = tunnel
|
self.ssh_tunnel = tunnel
|
||||||
|
@ -107,13 +131,23 @@ class RDPConnectionManager:
|
||||||
self.close_ssh_tunnel()
|
self.close_ssh_tunnel()
|
||||||
|
|
||||||
def check_rdp_connectivity(self):
|
def check_rdp_connectivity(self):
|
||||||
connect_params = list(self.connection_details.values()) + ['', 0]
|
connect_params = [
|
||||||
is_reachable = pyfreerdp.check_connectivity(*connect_params)
|
self.connection_details['hostname'],
|
||||||
|
self.connection_details['port'],
|
||||||
|
self.connection_details['username'],
|
||||||
|
self.connection_details['password'],
|
||||||
|
'', # extra parameter (if needed)
|
||||||
|
0 # timeout (if needed)
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
is_reachable = pyfreerdp.check_connectivity(*connect_params)
|
||||||
|
except Exception as ex:
|
||||||
|
is_reachable = False
|
||||||
self.result_queue.put(is_reachable)
|
self.result_queue.put(is_reachable)
|
||||||
|
|
||||||
def attempt_connection(self):
|
def attempt_connection(self):
|
||||||
if self.params['login_secret_type'] != 'password':
|
if self.params['login_secret_type'] != 'password':
|
||||||
error_message = f'unsupported authentication method: {self.params["login_secret_type"]}'
|
error_message = f"Unsupported authentication method: {self.params['login_secret_type']}"
|
||||||
return False, error_message
|
return False, error_message
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -138,17 +172,21 @@ class RDPConnectionManager:
|
||||||
def main():
|
def main():
|
||||||
argument_spec = common_argument_spec()
|
argument_spec = common_argument_spec()
|
||||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||||
result = {'changed': False}
|
rdp_manager = RDPConnectionManager(module.params)
|
||||||
module_params = module.params
|
|
||||||
rdp_manager = RDPConnectionManager(module_params)
|
|
||||||
is_available, error_message = rdp_manager.attempt_connection()
|
is_available, error_message = rdp_manager.attempt_connection()
|
||||||
result['is_available'] = is_available
|
|
||||||
|
# Prepare the result structure.
|
||||||
|
result = {
|
||||||
|
'changed': False,
|
||||||
|
'is_available': is_available,
|
||||||
|
'conn_err_msg': error_message
|
||||||
|
}
|
||||||
|
|
||||||
if not is_available:
|
if not is_available:
|
||||||
module.fail_json(msg=f'Unable to connect to asset: {error_message}')
|
module.fail_json(msg=f"Unable to connect to asset: {error_message}", **result)
|
||||||
|
else:
|
||||||
return module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -4,18 +4,35 @@ from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
module: ssh_ping
|
module: ssh_ping
|
||||||
short_description: Use ssh to probe whether an asset is connectable
|
short_description: Use ssh to probe whether an asset is connectable
|
||||||
description:
|
description:
|
||||||
- Use ssh to probe whether an asset is connectable
|
- Use ssh to probe whether an asset is connectable.
|
||||||
|
options:
|
||||||
|
login_host:
|
||||||
|
description: The target host to connect.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
login_port:
|
||||||
|
description: The port on the target host.
|
||||||
|
type: int
|
||||||
|
required: False
|
||||||
|
default: 22
|
||||||
|
login_user:
|
||||||
|
description: The username for the SSH connection.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
login_password:
|
||||||
|
description: The password for the SSH connection.
|
||||||
|
type: str
|
||||||
|
required: True
|
||||||
|
no_log: True
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
- name: >
|
- name: Ping asset server using SSH.
|
||||||
Ping asset server.
|
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_host: 127.0.0.1
|
login_host: 127.0.0.1
|
||||||
login_port: 22
|
login_port: 22
|
||||||
|
@ -25,39 +42,27 @@ EXAMPLES = '''
|
||||||
|
|
||||||
RETURN = '''
|
RETURN = '''
|
||||||
is_available:
|
is_available:
|
||||||
description: Ping server availability.
|
description: Indicate whether the target server is reachable via SSH.
|
||||||
returned: always
|
returned: always
|
||||||
type: bool
|
type: bool
|
||||||
sample: true
|
sample: true
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from libs.ansible.modules_utils.remote_client import SSHClient, common_argument_spec
|
||||||
from libs.ansible.modules_utils.custom_common import (
|
|
||||||
SSHClient, common_argument_spec
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================================
|
|
||||||
# Module execution.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
options = common_argument_spec()
|
options = common_argument_spec()
|
||||||
module = AnsibleModule(argument_spec=options, supports_check_mode=True,)
|
module = AnsibleModule(argument_spec=options, supports_check_mode=True)
|
||||||
|
|
||||||
result = {
|
result = {'changed': False, 'is_available': False}
|
||||||
'changed': False, 'is_available': True
|
|
||||||
}
|
|
||||||
client = SSHClient(module)
|
|
||||||
err = client.connect()
|
|
||||||
if err:
|
|
||||||
module.fail_json(msg='Unable to connect to asset: %s' % err)
|
|
||||||
result['is_available'] = False
|
|
||||||
|
|
||||||
return module.exit_json(**result)
|
with SSHClient(module) as client:
|
||||||
|
client.connect()
|
||||||
|
|
||||||
|
result['is_available'] = True
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
import paramiko
|
|
||||||
from sshtunnel import SSHTunnelForwarder
|
|
||||||
|
|
||||||
|
|
||||||
class OldSSHTransport(paramiko.transport.Transport):
|
|
||||||
_preferred_pubkeys = (
|
|
||||||
"ssh-ed25519",
|
|
||||||
"ecdsa-sha2-nistp256",
|
|
||||||
"ecdsa-sha2-nistp384",
|
|
||||||
"ecdsa-sha2-nistp521",
|
|
||||||
"ssh-rsa",
|
|
||||||
"rsa-sha2-256",
|
|
||||||
"rsa-sha2-512",
|
|
||||||
"ssh-dss",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def common_argument_spec():
|
|
||||||
options = dict(
|
|
||||||
login_host=dict(type='str', required=False, default='localhost'),
|
|
||||||
login_port=dict(type='int', required=False, default=22),
|
|
||||||
login_user=dict(type='str', required=False, default='root'),
|
|
||||||
login_password=dict(type='str', required=False, no_log=True),
|
|
||||||
login_secret_type=dict(type='str', required=False, default='password'),
|
|
||||||
login_private_key_path=dict(type='str', required=False, no_log=True),
|
|
||||||
first_conn_delay_time=dict(type='float', required=False, default=0.5),
|
|
||||||
gateway_args=dict(type='str', required=False, default=''),
|
|
||||||
|
|
||||||
become=dict(type='bool', default=False, required=False),
|
|
||||||
become_method=dict(type='str', required=False),
|
|
||||||
become_user=dict(type='str', required=False),
|
|
||||||
become_password=dict(type='str', required=False, no_log=True),
|
|
||||||
become_private_key_path=dict(type='str', required=False, no_log=True),
|
|
||||||
|
|
||||||
old_ssh_version=dict(type='bool', default=False, required=False),
|
|
||||||
)
|
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
class SSHClient:
|
|
||||||
TIMEOUT = 20
|
|
||||||
SLEEP_INTERVAL = 2
|
|
||||||
COMPLETE_FLAG = 'complete'
|
|
||||||
|
|
||||||
def __init__(self, module):
|
|
||||||
self.module = module
|
|
||||||
self.channel = None
|
|
||||||
self.is_connect = False
|
|
||||||
self.gateway_server = None
|
|
||||||
self.client = paramiko.SSHClient()
|
|
||||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
||||||
self.connect_params = self.get_connect_params()
|
|
||||||
|
|
||||||
def get_connect_params(self):
|
|
||||||
params = {
|
|
||||||
'allow_agent': False, 'look_for_keys': False,
|
|
||||||
'hostname': self.module.params['login_host'],
|
|
||||||
'port': self.module.params['login_port'],
|
|
||||||
'key_filename': self.module.params['login_private_key_path'] or None
|
|
||||||
}
|
|
||||||
if self.module.params['become']:
|
|
||||||
params['username'] = self.module.params['become_user']
|
|
||||||
params['password'] = self.module.params['become_password']
|
|
||||||
params['key_filename'] = self.module.params['become_private_key_path'] or None
|
|
||||||
else:
|
|
||||||
params['username'] = self.module.params['login_user']
|
|
||||||
params['password'] = self.module.params['login_password']
|
|
||||||
params['key_filename'] = self.module.params['login_private_key_path'] or None
|
|
||||||
if self.module.params['old_ssh_version']:
|
|
||||||
params['transport_factory'] = OldSSHTransport
|
|
||||||
return params
|
|
||||||
|
|
||||||
def _get_channel(self):
|
|
||||||
self.channel = self.client.invoke_shell()
|
|
||||||
# 读取首次登陆终端返回的消息
|
|
||||||
self.channel.recv(2048)
|
|
||||||
# 网络设备一般登录有延迟,等终端有返回后再执行命令
|
|
||||||
delay_time = self.module.params['first_conn_delay_time']
|
|
||||||
time.sleep(delay_time)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_match_user(user, content):
|
|
||||||
# 正常命令切割后是[命令,用户名,交互前缀]
|
|
||||||
content_list = content.split() if len(content.split()) >= 3 else None
|
|
||||||
return content_list and user in content_list
|
|
||||||
|
|
||||||
def switch_user(self):
|
|
||||||
self._get_channel()
|
|
||||||
if not self.module.params['become']:
|
|
||||||
return
|
|
||||||
method = self.module.params['become_method']
|
|
||||||
username = self.module.params['login_user']
|
|
||||||
if method == 'sudo':
|
|
||||||
switch_method = 'sudo su -'
|
|
||||||
password = self.module.params['become_password']
|
|
||||||
elif method == 'su':
|
|
||||||
switch_method = 'su -'
|
|
||||||
password = self.module.params['login_password']
|
|
||||||
else:
|
|
||||||
self.module.fail_json(msg='Become method %s not support' % method)
|
|
||||||
return
|
|
||||||
commands = [f'{switch_method} {username}', password]
|
|
||||||
su_output, err_msg = self.execute(commands)
|
|
||||||
if err_msg:
|
|
||||||
return err_msg
|
|
||||||
i_output, err_msg = self.execute(
|
|
||||||
[f'whoami && echo "{self.COMPLETE_FLAG}"'],
|
|
||||||
validate_output=True
|
|
||||||
)
|
|
||||||
if err_msg:
|
|
||||||
return err_msg
|
|
||||||
|
|
||||||
if self._is_match_user(username, i_output):
|
|
||||||
err_msg = ''
|
|
||||||
else:
|
|
||||||
err_msg = su_output
|
|
||||||
return err_msg
|
|
||||||
|
|
||||||
def local_gateway_prepare(self):
|
|
||||||
gateway_args = self.module.params['gateway_args'] or ''
|
|
||||||
pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \
|
|
||||||
r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'"
|
|
||||||
match = re.search(pattern, gateway_args)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
return
|
|
||||||
|
|
||||||
password, port, username, address, private_key_path = match.groups()
|
|
||||||
password = password if password else None
|
|
||||||
private_key_path = private_key_path if private_key_path else None
|
|
||||||
remote_hostname = self.module.params['login_host']
|
|
||||||
remote_port = self.module.params['login_port']
|
|
||||||
|
|
||||||
server = SSHTunnelForwarder(
|
|
||||||
(address, int(port)),
|
|
||||||
ssh_username=username,
|
|
||||||
ssh_password=password,
|
|
||||||
ssh_pkey=private_key_path,
|
|
||||||
remote_bind_address=(remote_hostname, remote_port)
|
|
||||||
)
|
|
||||||
|
|
||||||
server.start()
|
|
||||||
self.connect_params['hostname'] = '127.0.0.1'
|
|
||||||
self.connect_params['port'] = server.local_bind_port
|
|
||||||
self.gateway_server = server
|
|
||||||
|
|
||||||
def local_gateway_clean(self):
|
|
||||||
gateway_server = self.gateway_server
|
|
||||||
if not gateway_server:
|
|
||||||
return
|
|
||||||
|
|
||||||
gateway_server.stop()
|
|
||||||
|
|
||||||
def before_runner_start(self):
|
|
||||||
self.local_gateway_prepare()
|
|
||||||
|
|
||||||
def after_runner_end(self):
|
|
||||||
self.local_gateway_clean()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
try:
|
|
||||||
self.before_runner_start()
|
|
||||||
self.client.connect(**self.connect_params)
|
|
||||||
self.is_connect = True
|
|
||||||
err_msg = self.switch_user()
|
|
||||||
self.after_runner_end()
|
|
||||||
except Exception as err:
|
|
||||||
err_msg = str(err)
|
|
||||||
return err_msg
|
|
||||||
|
|
||||||
def _get_recv(self, size=1024, encoding='utf-8'):
|
|
||||||
output = self.channel.recv(size).decode(encoding)
|
|
||||||
return output
|
|
||||||
|
|
||||||
def execute(self, commands, validate_output=False):
|
|
||||||
if not self.is_connect:
|
|
||||||
self.connect()
|
|
||||||
output, error_msg = '', ''
|
|
||||||
try:
|
|
||||||
for command in commands:
|
|
||||||
self.channel.send(command + '\n')
|
|
||||||
if not validate_output:
|
|
||||||
time.sleep(self.SLEEP_INTERVAL)
|
|
||||||
output += self._get_recv()
|
|
||||||
continue
|
|
||||||
start_time = time.time()
|
|
||||||
while self.COMPLETE_FLAG not in output:
|
|
||||||
if time.time() - start_time > self.TIMEOUT:
|
|
||||||
error_msg = output
|
|
||||||
print("切换用户操作超时,跳出循环。")
|
|
||||||
break
|
|
||||||
time.sleep(self.SLEEP_INTERVAL)
|
|
||||||
received_output = self._get_recv().replace(f'"{self.COMPLETE_FLAG}"', '')
|
|
||||||
output += received_output
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
return output, error_msg
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
try:
|
|
||||||
self.channel.close()
|
|
||||||
self.client.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
|
@ -0,0 +1,277 @@
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
from sshtunnel import SSHTunnelForwarder
|
||||||
|
|
||||||
|
DEFAULT_RE = '.*'
|
||||||
|
SU_PROMPT_LOCALIZATIONS = [
|
||||||
|
'Password', '암호', 'パスワード', 'Adgangskode', 'Contraseña', 'Contrasenya',
|
||||||
|
'Hasło', 'Heslo', 'Jelszó', 'Lösenord', 'Mật khẩu', 'Mot de passe',
|
||||||
|
'Parola', 'Parool', 'Pasahitza', 'Passord', 'Passwort', 'Salasana',
|
||||||
|
'Sandi', 'Senha', 'Wachtwoord', 'ססמה', 'Лозинка', 'Парола', 'Пароль',
|
||||||
|
'गुप्तशब्द', 'शब्दकूट', 'సంకేతపదము', 'හස්පදය', '密码', '密碼', '口令',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_become_prompt_re():
|
||||||
|
pattern_segments = (r'(\w+\'s )?' + p for p in SU_PROMPT_LOCALIZATIONS)
|
||||||
|
prompt_pattern = "|".join(pattern_segments) + r' ?(:|:) ?'
|
||||||
|
return re.compile(prompt_pattern, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
become_prompt_re = get_become_prompt_re()
|
||||||
|
|
||||||
|
|
||||||
|
def common_argument_spec():
|
||||||
|
options = dict(
|
||||||
|
login_host=dict(type='str', required=False, default='localhost'),
|
||||||
|
login_port=dict(type='int', required=False, default=22),
|
||||||
|
login_user=dict(type='str', required=False, default='root'),
|
||||||
|
login_password=dict(type='str', required=False, no_log=True),
|
||||||
|
login_secret_type=dict(type='str', required=False, default='password'),
|
||||||
|
login_private_key_path=dict(type='str', required=False, no_log=True),
|
||||||
|
gateway_args=dict(type='str', required=False, default=''),
|
||||||
|
recv_timeout=dict(type='int', required=False, default=30),
|
||||||
|
delay_time=dict(type='int', required=False, default=2),
|
||||||
|
prompt=dict(type='str', required=False, default='.*'),
|
||||||
|
answers=dict(type='str', required=False, default='.*'),
|
||||||
|
commands=dict(type='raw', required=False),
|
||||||
|
|
||||||
|
become=dict(type='bool', default=False, required=False),
|
||||||
|
become_method=dict(type='str', required=False),
|
||||||
|
become_user=dict(type='str', required=False),
|
||||||
|
become_password=dict(type='str', required=False, no_log=True),
|
||||||
|
become_private_key_path=dict(type='str', required=False, no_log=True),
|
||||||
|
|
||||||
|
old_ssh_version=dict(type='bool', default=False, required=False),
|
||||||
|
)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def raise_timeout(name=''):
|
||||||
|
def decorate(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
def handler(signum, frame):
|
||||||
|
raise TimeoutError(f'{name} timed out, wait {timeout}s')
|
||||||
|
|
||||||
|
timeout = getattr(self, 'timeout', 0)
|
||||||
|
try:
|
||||||
|
if timeout > 0:
|
||||||
|
signal.signal(signal.SIGALRM, handler)
|
||||||
|
signal.alarm(timeout)
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except Exception as error:
|
||||||
|
signal.alarm(0)
|
||||||
|
raise error
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
class OldSSHTransport(paramiko.transport.Transport):
|
||||||
|
_preferred_pubkeys = (
|
||||||
|
"ssh-ed25519",
|
||||||
|
"ecdsa-sha2-nistp256",
|
||||||
|
"ecdsa-sha2-nistp384",
|
||||||
|
"ecdsa-sha2-nistp521",
|
||||||
|
"ssh-rsa",
|
||||||
|
"rsa-sha2-256",
|
||||||
|
"rsa-sha2-512",
|
||||||
|
"ssh-dss",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHClient:
|
||||||
|
def __init__(self, module):
|
||||||
|
self.module = module
|
||||||
|
self.gateway_server = None
|
||||||
|
self.client = paramiko.SSHClient()
|
||||||
|
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
self.connect_params = self.get_connect_params()
|
||||||
|
self._channel = None
|
||||||
|
|
||||||
|
self.buffer_size = 1024
|
||||||
|
self.prompt = self.module.params['prompt']
|
||||||
|
self.timeout = self.module.params['recv_timeout']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self):
|
||||||
|
if self._channel is None:
|
||||||
|
self.connect()
|
||||||
|
return self._channel
|
||||||
|
|
||||||
|
def get_connect_params(self):
|
||||||
|
p = self.module.params
|
||||||
|
params = {
|
||||||
|
'allow_agent': False,
|
||||||
|
'look_for_keys': False,
|
||||||
|
'hostname': p['login_host'],
|
||||||
|
'port': p['login_port'],
|
||||||
|
'key_filename': p['login_private_key_path'] or None
|
||||||
|
}
|
||||||
|
|
||||||
|
if p['become']:
|
||||||
|
params['username'] = p['become_user']
|
||||||
|
params['password'] = p['become_password']
|
||||||
|
params['key_filename'] = p['become_private_key_path'] or None
|
||||||
|
else:
|
||||||
|
params['username'] = p['login_user']
|
||||||
|
params['password'] = p['login_password']
|
||||||
|
params['key_filename'] = p['login_private_key_path'] or None
|
||||||
|
|
||||||
|
if p['old_ssh_version']:
|
||||||
|
params['transport_factory'] = OldSSHTransport
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def switch_user(self):
|
||||||
|
p = self.module.params
|
||||||
|
if not p['become']:
|
||||||
|
return
|
||||||
|
|
||||||
|
method = p['become_method']
|
||||||
|
username = p['login_user']
|
||||||
|
|
||||||
|
if method == 'sudo':
|
||||||
|
switch_cmd = 'sudo su -'
|
||||||
|
pword = p['become_password']
|
||||||
|
elif method == 'su':
|
||||||
|
switch_cmd = 'su -'
|
||||||
|
pword = p['login_password']
|
||||||
|
else:
|
||||||
|
self.module.fail_json(msg=f'Become method {method} not supported.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Expected to see a prompt, type the password, and check the username
|
||||||
|
output, error = self.execute(
|
||||||
|
[f'{switch_cmd} {username}', pword, 'whoami'],
|
||||||
|
[become_prompt_re, DEFAULT_RE, username]
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
self.module.fail_json(msg=f'Failed to become user {username}. Output: {output}')
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.before_runner_start()
|
||||||
|
try:
|
||||||
|
self.client.connect(**self.connect_params)
|
||||||
|
self._channel = self.client.invoke_shell()
|
||||||
|
self._get_match_recv()
|
||||||
|
self.switch_user()
|
||||||
|
except Exception as error:
|
||||||
|
self.module.fail_json(msg=str(error))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fit_answers(commands, answers):
|
||||||
|
if answers is None or not isinstance(answers, list):
|
||||||
|
answers = [DEFAULT_RE] * len(commands)
|
||||||
|
elif len(answers) < len(commands):
|
||||||
|
answers += [DEFAULT_RE] * (len(commands) - len(answers))
|
||||||
|
return answers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __match(expression, content):
|
||||||
|
if isinstance(expression, str):
|
||||||
|
expression = re.compile(expression, re.DOTALL | re.IGNORECASE)
|
||||||
|
elif not isinstance(expression, re.Pattern):
|
||||||
|
raise ValueError(f'{expression} should be a regular expression')
|
||||||
|
|
||||||
|
return bool(expression.search(content))
|
||||||
|
|
||||||
|
@raise_timeout('Recv message')
|
||||||
|
def _get_match_recv(self, answer_reg=DEFAULT_RE):
|
||||||
|
buffer_str = ''
|
||||||
|
prev_str = ''
|
||||||
|
|
||||||
|
check_reg = self.prompt if answer_reg == DEFAULT_RE else answer_reg
|
||||||
|
while True:
|
||||||
|
if self.channel.recv_ready():
|
||||||
|
chunk = self.channel.recv(self.buffer_size).decode('utf-8', 'replace')
|
||||||
|
buffer_str += chunk
|
||||||
|
|
||||||
|
if buffer_str and buffer_str != prev_str:
|
||||||
|
if self.__match(check_reg, buffer_str):
|
||||||
|
break
|
||||||
|
prev_str = buffer_str
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
return buffer_str
|
||||||
|
|
||||||
|
@raise_timeout('Wait send message')
|
||||||
|
def _check_send(self):
|
||||||
|
while not self.channel.send_ready():
|
||||||
|
time.sleep(0.01)
|
||||||
|
time.sleep(self.module.params['delay_time'])
|
||||||
|
|
||||||
|
def execute(self, commands, answers=None):
|
||||||
|
combined_output = ''
|
||||||
|
error_msg = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
answers = self._fit_answers(commands, answers)
|
||||||
|
for cmd, ans_regex in zip(commands, answers):
|
||||||
|
self._check_send()
|
||||||
|
self.channel.send(cmd + '\n')
|
||||||
|
combined_output += self._get_match_recv(ans_regex) + '\n'
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
|
||||||
|
return combined_output, error_msg
|
||||||
|
|
||||||
|
def local_gateway_prepare(self):
|
||||||
|
gateway_args = self.module.params['gateway_args'] or ''
|
||||||
|
pattern = (
|
||||||
|
r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+"
|
||||||
|
r"([\w@]+)@([\d.]+)\s+-W %h:%p -q(?: -i (.+))?'"
|
||||||
|
)
|
||||||
|
match = re.search(pattern, gateway_args)
|
||||||
|
if not match:
|
||||||
|
return
|
||||||
|
|
||||||
|
password, port, username, remote_addr, key_path = match.groups()
|
||||||
|
password = password or None
|
||||||
|
key_path = key_path or None
|
||||||
|
|
||||||
|
server = SSHTunnelForwarder(
|
||||||
|
(remote_addr, int(port)),
|
||||||
|
ssh_username=username,
|
||||||
|
ssh_password=password,
|
||||||
|
ssh_pkey=key_path,
|
||||||
|
remote_bind_address=(
|
||||||
|
self.module.params['login_host'],
|
||||||
|
self.module.params['login_port']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
server.start()
|
||||||
|
self.connect_params['hostname'] = '127.0.0.1'
|
||||||
|
self.connect_params['port'] = server.local_bind_port
|
||||||
|
self.gateway_server = server
|
||||||
|
|
||||||
|
def local_gateway_clean(self):
|
||||||
|
if self.gateway_server:
|
||||||
|
self.gateway_server.stop()
|
||||||
|
|
||||||
|
def before_runner_start(self):
|
||||||
|
self.local_gateway_prepare()
|
||||||
|
|
||||||
|
def after_runner_end(self):
|
||||||
|
self.local_gateway_clean()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
try:
|
||||||
|
self.after_runner_end()
|
||||||
|
if self.channel:
|
||||||
|
self.channel.close()
|
||||||
|
if self.client:
|
||||||
|
self.client.close()
|
||||||
|
except Exception: # noqa
|
||||||
|
pass
|
Loading…
Reference in New Issue