diff --git a/cmd/climc/shell/webconsole.go b/cmd/climc/shell/webconsole.go index 0a784dc555..e8d4a26d9f 100644 --- a/cmd/climc/shell/webconsole.go +++ b/cmd/climc/shell/webconsole.go @@ -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 + }) } diff --git a/pkg/mcclient/modules/mod_webconsole.go b/pkg/mcclient/modules/mod_webconsole.go index 0c1b79f985..963240028d 100644 --- a/pkg/mcclient/modules/mod_webconsole.go +++ b/pkg/mcclient/modules/mod_webconsole.go @@ -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) +} diff --git a/pkg/mcclient/options/webconsole.go b/pkg/mcclient/options/webconsole.go index 723c03da6f..ba9a36db14 100644 --- a/pkg/mcclient/options/webconsole.go +++ b/pkg/mcclient/options/webconsole.go @@ -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"` +} diff --git a/pkg/webconsole/command/command.go b/pkg/webconsole/command/command.go index 35ed12f53d..faccd18e88 100644 --- a/pkg/webconsole/command/command.go +++ b/pkg/webconsole/command/command.go @@ -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 diff --git a/pkg/webconsole/command/ssh_command.go b/pkg/webconsole/command/ssh_command.go new file mode 100644 index 0000000000..8bd6cce9c1 --- /dev/null +++ b/pkg/webconsole/command/ssh_command.go @@ -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 +} diff --git a/pkg/webconsole/handlers.go b/pkg/webconsole/handlers.go index cd44f4f0c0..39f5627924 100644 --- a/pkg/webconsole/handlers.go +++ b/pkg/webconsole/handlers.go @@ -31,6 +31,7 @@ func InitHandlers(app *appsrv.Application) { app.AddHandler("POST", ApiPathPrefix+"k8s//shell", auth.Authenticate(handleK8sShell)) app.AddHandler("POST", ApiPathPrefix+"k8s//log", auth.Authenticate(handleK8sLog)) app.AddHandler("POST", ApiPathPrefix+"baremetal/", auth.Authenticate(handleBaremetalShell)) + app.AddHandler("POST", ApiPathPrefix+"ssh/", 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[""]) + 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 { diff --git a/pkg/webconsole/options/options.go b/pkg/webconsole/options/options.go index 45e9235f00..d881ea9386 100644 --- a/pkg/webconsole/options/options.go +++ b/pkg/webconsole/options/options.go @@ -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"` } diff --git a/pkg/webconsole/server/tty_server.go b/pkg/webconsole/server/tty_server.go index 717f7d1fa4..4cf4f0757d 100644 --- a/pkg/webconsole/server/tty_server.go +++ b/pkg/webconsole/server/tty_server.go @@ -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 } diff --git a/pkg/webconsole/service/service.go b/pkg/webconsole/service/service.go index 3836f618bb..3cf4f09adb 100644 --- a/pkg/webconsole/service/service.go +++ b/pkg/webconsole/service/service.go @@ -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) } diff --git a/pkg/webconsole/session/pty_session.go b/pkg/webconsole/session/pty_session.go index c28d0e6542..7cb2ea8182 100644 --- a/pkg/webconsole/session/pty_session.go +++ b/pkg/webconsole/session/pty_session.go @@ -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() }