Automatic merge from release/2.1.0 -> release/2.2.0

* commit 'dcf7c00dcb3725e7745036e7c977d6fe4e06bcd7':
  添加超时处理
  添加admin参数
  支持ssh webconsole
This commit is contained in:
邱剑
2018-09-30 10:15:24 +08:00
10 changed files with 213 additions and 29 deletions
+9
View File
@@ -65,4 +65,13 @@ func init() {
handleResult(args.WebConsoleOptions, ret)
return nil
})
R(&o.WebConsoleSshOptions{}, "webconsole-ssh", "Connect ssh webconsole", func(s *mcclient.ClientSession, args *o.WebConsoleSshOptions) error {
ret, err := modules.WebConsole.DoSshConnect(s, args.IP)
if err != nil {
return err
}
handleResult(args.WebConsoleOptions, ret)
return nil
})
}
+4
View File
@@ -69,3 +69,7 @@ func (m WebConsoleManager) DoK8sLogConnect(
func (m WebConsoleManager) DoBaremetalConnect(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) {
return m.DoConnect(s, "baremetal", id, "", nil)
}
func (m WebConsoleManager) DoSshConnect(s *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) {
return m.DoConnect(s, "ssh", id, "", nil)
}
+5
View File
@@ -36,3 +36,8 @@ type WebConsoleBaremetalOptions struct {
func (opt *WebConsoleBaremetalOptions) Params() (*jsonutils.JSONDict, error) {
return StructToParams(opt)
}
type WebConsoleSshOptions struct {
WebConsoleOptions
IP string `help:"IP to connect"`
}
+10
View File
@@ -15,6 +15,8 @@ type ICommand interface {
GetProtocol() string
GetCommand() *exec.Cmd
Cleanup() error
GetData(string) (isShow bool, ouput string, command string)
ShowInfo() string
}
type BaseCommand struct {
@@ -40,6 +42,14 @@ func (c BaseCommand) GetCommand() *exec.Cmd {
return exec.Command(c.name, c.args...)
}
func (c BaseCommand) GetData(comand string) (isShow bool, ouput string, command string) {
return true, "", ""
}
func (c BaseCommand) ShowInfo() string {
return ""
}
func (c BaseCommand) Cleanup() error {
log.Infof("BaseCommand Cleanup do nothing")
return nil
+72
View File
@@ -0,0 +1,72 @@
package command
import (
"fmt"
"net"
"os/exec"
"time"
"yunion.io/x/log"
o "yunion.io/x/onecloud/pkg/webconsole/options"
)
type SSHtoolSol struct {
*BaseCommand
IP string
Username string
reTry int
showInfo string
}
func NewSSHtoolSolCommand(ip string) (*SSHtoolSol, error) {
if conn, err := net.DialTimeout("tcp", ip+":22", time.Second*2); err != nil {
return nil, fmt.Errorf("IPAddress %s not accessable", ip)
} else {
conn.Close()
return &SSHtoolSol{
BaseCommand: nil,
IP: ip,
Username: "",
reTry: 0,
showInfo: fmt.Sprintf("%s login:", ip),
}, nil
}
}
func (c *SSHtoolSol) GetCommand() *exec.Cmd {
return nil
}
func (c *SSHtoolSol) Cleanup() error {
log.Infof("SSHtoolSol Cleanup do nothing")
return nil
}
func (c *SSHtoolSol) GetProtocol() string {
return PROTOCOL_TTY
}
func (c *SSHtoolSol) GetData(data string) (isShow bool, ouput string, command string) {
if len(c.Username) == 0 {
if len(data) == 0 {
//用户名不能为空
return true, c.showInfo, ""
}
c.Username = data
return false, "Password:", ""
} else {
return true, "", fmt.Sprintf("%s -p %s %s %s@%s", o.Options.SshpassToolPath, data, o.Options.SshToolPath, c.Username, c.IP)
}
}
func (c *SSHtoolSol) ShowInfo() string {
c.Username = ""
c.reTry++
if c.reTry == 3 {
c.reTry = 0
//清屏
time.Sleep(1 * time.Second)
return "\033c " + c.showInfo
}
return c.showInfo
}
+16 -1
View File
@@ -31,6 +31,7 @@ func InitHandlers(app *appsrv.Application) {
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/shell", auth.Authenticate(handleK8sShell))
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/log", auth.Authenticate(handleK8sLog))
app.AddHandler("POST", ApiPathPrefix+"baremetal/<id>", auth.Authenticate(handleBaremetalShell))
app.AddHandler("POST", ApiPathPrefix+"ssh/<ip>", auth.Authenticate(handleSshShell))
}
func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) {
@@ -122,7 +123,7 @@ func fetchCloudEnv(ctx context.Context, w http.ResponseWriter, r *http.Request)
if userCred == nil {
return nil, httperrors.NewUnauthorizedError("No token founded")
}
s := auth.Client().NewSession(o.Options.Region, "", "internal", userCred, "")
s := auth.Client().NewSession(o.Options.Region, "", "internal", userCred, "v2")
return &CloudEnv{
ClientSessin: s,
Params: params,
@@ -156,6 +157,20 @@ func handleK8sLog(ctx context.Context, w http.ResponseWriter, r *http.Request) {
handleK8sCommand(ctx, w, r, command.NewPodLogCommand)
}
func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
cmd, err := command.NewSSHtoolSolCommand(env.Params["<ip>"])
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
handleCommandSession(cmd, w)
}
func handleBaremetalShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
env, err := fetchCloudEnv(ctx, w, r)
if err != nil {
+5 -3
View File
@@ -11,7 +11,9 @@ var (
type WebConsoleOptions struct {
cloudcommon.Options
ApiServer string `help:"API server url to handle websocket connection, usually with public access" default:"http://webconsole.yunion.io"`
KubectlPath string `help:"kubectl binary path used to connect k8s cluster" default:"/usr/bin/kubectl"`
IpmitoolPath string `help:"ipmitool binary path used to connect baremetal sol" default:"/usr/bin/ipmitool"`
ApiServer string `help:"API server url to handle websocket connection, usually with public access" default:"http://webconsole.yunion.io"`
KubectlPath string `help:"kubectl binary path used to connect k8s cluster" default:"/usr/bin/kubectl"`
IpmitoolPath string `help:"ipmitool binary path used to connect baremetal sol" default:"/usr/bin/ipmitool"`
SshToolPath string `help:"sshtool binary path used to connect server sol" default:"/usr/bin/ssh"`
SshpassToolPath string `help:"sshpass tool binary path used to connect server sol" default:"/usr/local/bin/sshpass"`
}
+60 -6
View File
@@ -1,6 +1,11 @@
package server
import (
"os/exec"
"strconv"
"strings"
"time"
socketio "github.com/googollee/go-socket.io"
"github.com/kr/pty"
@@ -56,19 +61,67 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
go func() {
buf := make([]byte, 1024)
for {
n, err := p.Pty.Read(buf)
if err != nil {
log.Errorf("Failed to read from pty master: %v", err)
cleanUp(so, p)
if p.IsOk {
if p.Cmd == nil || p.Cmd.Process == nil {
p.IsOk = false
} else if p.Pty == nil {
p.IsOk = false
} else if n, err := p.Pty.Read(buf); err != nil {
p.IsOk = false
} else {
so.Emit(OUTPUT_EVENT, string(buf[0:n]))
}
if !p.IsOk {
p.Stop()
if info := p.Session.ShowInfo(); len(info) > 0 {
so.Emit(OUTPUT_EVENT, info)
}
}
} else if p.Exit {
return
} else {
//避免goroutine死循环导致主进程卡死
time.Sleep(time.Microsecond * 50)
}
so.Emit(OUTPUT_EVENT, string(buf[0:n]))
}
}()
// handle write
so.On(INPUT_EVENT, func(data string) {
p.Pty.Write([]byte(data))
if !p.IsOk {
if data == "\r" {
p.Show, p.Output, p.Command = p.Session.GetData(p.Buffer)
so.Emit(OUTPUT_EVENT, "\r\n")
if len(p.Output) > 0 {
so.Emit(OUTPUT_EVENT, p.Output)
}
if len(p.Command) > 0 {
log.Infof("exec: %s", p.Command)
args := strings.Split(p.Command, " ")
cmd := exec.Command(args[0], args[1:]...)
if _pty, err := pty.Start(cmd); err != nil {
so.Emit(OUTPUT_EVENT, err.Error()+"\r\n")
log.Errorf("exec error: %v", err)
} else {
p.Pty, p.Cmd, p.IsOk = _pty, cmd, true
}
}
p.Buffer, data = "", ""
} else if data == "\u007f" {
//退格处理
if len(p.Buffer) > 0 {
p.Buffer = p.Buffer[:len(p.Buffer)-1]
data = "\b \b"
}
} else if strconv.IsPrint([]rune(data)[0]) {
p.Buffer += data
}
if p.Show && len(data) > 0 {
so.Emit(OUTPUT_EVENT, data)
}
} else {
p.Pty.Write([]byte(data))
}
})
// handle resize
@@ -106,4 +159,5 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
func cleanUp(so socketio.Socket, p *session.Pty) {
so.Disconnect()
p.Stop()
p.Exit = true
}
+1 -1
View File
@@ -34,7 +34,7 @@ func StartService() {
log.Fatalf("invalid --api-server %s", o.Options.ApiServer)
}
for _, binPath := range []string{o.Options.KubectlPath, o.Options.IpmitoolPath} {
for _, binPath := range []string{o.Options.KubectlPath, o.Options.IpmitoolPath, o.Options.SshToolPath, o.Options.SshpassToolPath} {
ensureBinExists(binPath)
}
+31 -18
View File
@@ -17,6 +17,12 @@ type Pty struct {
Pty *os.File
sizeCh chan os.Signal
size *pty.Winsize
Show bool
IsOk bool
Buffer string
Output string
Command string
Exit bool
}
func NewPty(session *SSession) (p *Pty, err error) {
@@ -24,11 +30,17 @@ func NewPty(session *SSession) (p *Pty, err error) {
p = &Pty{
Session: session,
Cmd: cmd,
Show: true,
IsOk: true,
Exit: false,
Pty: nil,
}
log.Debugf("[session %s] Start command: %#v", session.Id, cmd)
p.Pty, err = pty.Start(p.Cmd)
if err != nil {
return
if cmd != nil {
p.Pty, err = pty.Start(p.Cmd)
if err != nil {
return
}
}
p.sizeCh = make(chan os.Signal, 1)
p.size = &pty.Winsize{}
@@ -40,10 +52,12 @@ func NewPty(session *SSession) (p *Pty, err error) {
func (p *Pty) startResizeMonitor() {
go func() {
for range p.sizeCh {
if err := pty.Setsize(p.Pty, p.size); err != nil {
log.Errorf("Resize pty error: %v", err)
} else {
log.Debugf("Resize pty to %#v, cmd: %#v", p.size, p.Cmd)
if p.Pty != nil {
if err := pty.Setsize(p.Pty, p.size); err != nil {
log.Errorf("Resize pty error: %v", err)
} else {
log.Debugf("Resize pty to %#v, cmd: %#v", p.size, p.Cmd)
}
}
}
}()
@@ -55,18 +69,17 @@ func (p *Pty) Resize(size *pty.Winsize) {
}
func (p *Pty) Stop() {
var err error
err = p.Pty.Close()
if err != nil {
log.Errorf("Close PTY error: %v", err)
if p.Pty != nil {
if err := p.Pty.Close(); err != nil {
log.Errorf("Close PTY error: %v", err)
}
}
err = p.Cmd.Process.Signal(os.Kill)
if err != nil {
log.Errorf("Kill command process error: %v", err)
}
err = p.Cmd.Wait()
if err != nil {
log.Errorf("Wait command error: %v", err)
if p.Cmd != nil && p.Cmd.Process != nil {
if err := p.Cmd.Process.Signal(os.Kill); err != nil {
log.Errorf("Kill command process error: %v", err)
} else if err := p.Cmd.Wait(); err != nil {
log.Errorf("Wait command error: %v", err)
}
}
p.Session.Close()
}