feat : add issue 6787 (#2764)

feat : add issue 6787



feat : add issue 6787



feat : add issue 6787



feat : add issue 6787



feat : feat issue 6787



feat: add issue 6787

Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com>
This commit is contained in:
zuoxuesong-worker 2025-09-19 09:43:52 +08:00 committed by GitHub
parent 6e4fc9956e
commit 54e819d867
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 215 additions and 4 deletions

View File

@ -110,3 +110,16 @@ const ( // === From CAPKK base on GetCAPKKProject() ===
// CAPKKPlaybookDeleteNode is the playbook for delete node.
CAPKKPlaybookDeleteNode = "playbooks/delete_node.yaml"
)
const (
// SSHVerifyStatusSuccess means ssh connect success
SSHVerifyStatusSuccess = "success"
// SSHVerifyStatusFailed means ssh connect failed
SSHVerifyStatusFailed = "ssh_failed"
// SSHVerifyStatusOffline means ssh target offline
SSHVerifyStatusOffline = "offline"
// SSHVerifyStatusSSHIncomplete means ssh connect information incomplete
SSHVerifyStatusSSHIncomplete = "ssh_incomplete"
// SSHVerifyStatusUnreachable means host server cannot connect to target ssh
SSHVerifyStatusUnreachable = "unreachable"
)

View File

@ -186,6 +186,22 @@ type IPTable struct {
Added bool `json:"added"` // Indicates whether this IP has already been added to the inventory
}
// IPHostCheckData represents an IP address entry and its SSH connect data information.
type IPHostCheckData struct {
IP string `json:"ip"`
SSHPort string `json:"sshPort"`
SSHUser string `json:"sshUser"`
SSHPwd string `json:"sshPwd"`
SSHPrivateKeyContent string `json:"sshPrivateKeyContent"`
}
// IPHostCheckResult returns ip host check result
type IPHostCheckResult struct {
IP string `json:"ip"`
SSHPort string `json:"sshPort"`
Status string `json:"status"`
}
// SchemaFile2Table converts a SchemaFile and its filename into a SchemaTable structure.
// It initializes the SchemaTable fields from the SchemaFile's DataSchema and sets up the Playbook map
// with playbook labels and their corresponding paths. Other playbook fields are left empty for later population.

View File

@ -224,12 +224,12 @@ func (h ResourceHandler) ListIP(request *restful.Request, response *restful.Resp
for range maxConcurrency {
wg.Start(func() {
for ip := range jobChannel {
var added = false
if _, ok := addedPorts[ip+":"+sshPort]; ok {
added = true
}
if utils.IsLocalhostIP(ip) {
mu.Lock()
var added = false
if _, ok := addedPorts[ip+":"+sshPort]; ok {
added = true
}
ipTable = append(ipTable, api.IPTable{
IP: ip,
SSHPort: sshPort,
@ -255,6 +255,7 @@ func (h ResourceHandler) ListIP(request *restful.Request, response *restful.Resp
SSHPort: sshPort,
SSHReachable: reachable,
SSHAuthorized: authorized,
Added: added,
})
mu.Unlock()
}
@ -641,3 +642,179 @@ func isSSHAuthorized(ipStr, sshPort string) (bool, bool) {
// Port 22 is reachable and SSH authentication succeeded.
return true, true
}
func checkSSHConnect(ipStr, sshPort, sshUser, sshPwd, sshPrivateKeyContent string) (bool, bool) {
conn, err := net.DialTimeout("tcp", net.JoinHostPort(ipStr, sshPort), time.Second)
if err != nil {
klog.V(6).Infof("port %s not reachable on ip %q, error %v", sshPort, ipStr, err)
return false, false
}
defer conn.Close()
var authMethods []ssh.AuthMethod
if sshPwd != "" {
authMethods = append(authMethods, ssh.Password(sshPwd))
klog.V(6).Infof("Added password authentication for user %s", sshUser)
}
if sshPrivateKeyContent != "" {
signer, err := ssh.ParsePrivateKey([]byte(sshPrivateKeyContent))
if err != nil {
klog.V(6).Infof("Failed to parse provided private key: %v", err)
} else {
authMethods = append(authMethods, ssh.PublicKeys(signer))
klog.V(6).Infof("Added public key authentication from provided content for user %s", sshUser)
}
} else {
klog.V(6).Infof("No private key content provided, checking for default private keys")
foundKeys := findSSHPrivateKeys()
if len(foundKeys) > 0 {
klog.V(6).Infof("Found %d potential private key files", len(foundKeys))
for _, keyPath := range foundKeys {
keyBytes, err := os.ReadFile(keyPath)
if err != nil {
klog.V(6).Infof("Failed to read private key file %s: %v", keyPath, err)
continue
}
signer, err := ssh.ParsePrivateKey(keyBytes)
if err != nil {
klog.V(6).Infof("Failed to parse private key from %s: %v", keyPath, err)
continue
}
authMethods = append(authMethods, ssh.PublicKeys(signer))
klog.V(6).Infof("Added public key authentication from %s for user %s", keyPath, sshUser)
// stop when one correct key found
break
}
} else {
klog.V(6).Infof("No default private key files found")
}
}
if len(authMethods) == 0 {
klog.V(6).Infof("No authentication methods available for user %s", sshUser)
return true, false
}
klog.V(6).Infof("Using %d authentication methods for SSH connection", len(authMethods))
config := &ssh.ClientConfig{
User: sshUser,
Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
sshClient, err := ssh.Dial("tcp", net.JoinHostPort(ipStr, sshPort), config)
if err != nil {
klog.V(6).Infof("SSH connection failed: %v", err)
return true, false
}
defer sshClient.Close()
session, err := sshClient.NewSession()
if err != nil {
klog.V(6).Infof("SSH session creation failed: %v", err)
return true, false
}
defer session.Close()
err = session.Run("echo 'SSH connection test'")
if err != nil {
klog.V(6).Infof("SSH command execution failed: %v", err)
return true, false
}
klog.V(6).Infof("SSH connection successful for user %s", sshUser)
return true, true
}
func findSSHPrivateKeys() []string {
var keyFiles = make([]string, 0)
homeDir, err := os.UserHomeDir()
if err != nil {
klog.V(6).Infof("Failed to get user home directory: %v", err)
homeDir = "/root"
}
sshDir := filepath.Join(homeDir, ".ssh")
var sshDirInfo os.FileInfo
if sshDirInfo, err = os.Stat(sshDir); os.IsNotExist(err) {
klog.V(6).Infof("SSH directory %s does not exist", sshDir)
return keyFiles
}
if !sshDirInfo.IsDir() {
return keyFiles
}
dirs, _ := os.ReadDir(sshDirInfo.Name())
for _, dir := range dirs {
if dir.IsDir() {
continue
}
keyFiles = append(keyFiles, dir.Name())
}
return keyFiles
}
// PreCheckHost check input ssh information.
func (h ResourceHandler) PreCheckHost(request *restful.Request, response *restful.Response) {
var hosts []api.IPHostCheckData
if err := request.ReadEntity(&hosts); err != nil {
api.HandleError(response, request, err)
return
}
var wg = sync.WaitGroup{}
var result = make([]api.IPHostCheckResult, len(hosts))
wg.Add(len(hosts))
for i, host := range hosts {
go func(idx int, currentHost api.IPHostCheckData) {
defer wg.Done()
var status string
if utils.IsLocalhostIP(currentHost.IP) {
status = _const.SSHVerifyStatusSuccess
}
if !isIPOnline(currentHost.IP) {
status = _const.SSHVerifyStatusOffline
}
if currentHost.SSHUser == "" {
status = _const.SSHVerifyStatusSSHIncomplete
}
if status == "" {
reachable, authorized := checkSSHConnect(currentHost.IP, currentHost.SSHPort,
currentHost.SSHUser, currentHost.SSHPwd, currentHost.SSHPrivateKeyContent)
switch {
case authorized && reachable:
status = _const.SSHVerifyStatusSuccess
case !authorized && reachable:
status = _const.SSHVerifyStatusFailed
case !reachable && !authorized:
status = _const.SSHVerifyStatusUnreachable
default:
klog.Warningf("check ssh connect show authorized but unreachable! ip:%s,port=%s",
currentHost.IP, currentHost.SSHPort)
status = _const.SSHVerifyStatusFailed
}
}
// if ssh_failed, it means current host can access target host but unauthorized
// in this case,if user did not input pwd or key,it means ssh information incomplete
if status == _const.SSHVerifyStatusFailed && currentHost.SSHPwd == "" && currentHost.SSHPrivateKeyContent == "" {
status = _const.SSHVerifyStatusSSHIncomplete
}
result[idx] = api.IPHostCheckResult{
IP: currentHost.IP,
SSHPort: currentHost.SSHPort,
Status: status,
}
}(i, host)
}
wg.Wait()
_ = response.WriteEntity(result)
}

View File

@ -193,6 +193,11 @@ func NewSchemaService(rootPath string, workdir string, client ctrlclient.Client)
Doc("get user-defined configuration information").
Metadata(restfulspec.KeyOpenAPITags, []string{api.ResourceTag}))
ws.Route(ws.POST("/ip").To(resourceHandler.PreCheckHost).
Doc("pre check host ssh connect information").
Metadata(restfulspec.KeyOpenAPITags, []string{api.ResourceTag}).
Returns(http.StatusOK, api.StatusOK, api.ListResult[api.IPHostCheckResult]{}))
return ws
}