diff --git a/build/climc/root/opt/climc-entrypoint.sh b/build/climc/root/opt/climc-entrypoint.sh new file mode 100644 index 0000000000..b6cf286fb8 --- /dev/null +++ b/build/climc/root/opt/climc-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +BASHRC=/etc/profile.d/climc.sh + +grep -q rcadmin $BASHRC || echo 'source /etc/yunion/rcadmin' >> $BASHRC + +source $BASHRC + +climc sshkeypair-inject --admin --target-dir /root/ + +mkdir -p /etc/dropbear + +mkdir -p /var/run/dropbear + +test -f /etc/dropbear/dropbear_rsa_host_key || dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key + +dropbear -RFEjk -G root -p 22 diff --git a/build/climc/root/opt/yunion/scripts/motd/climc.sh b/build/climc/root/opt/yunion/scripts/motd/climc.sh index a726941823..1357be70c4 100644 --- a/build/climc/root/opt/yunion/scripts/motd/climc.sh +++ b/build/climc/root/opt/yunion/scripts/motd/climc.sh @@ -1,3 +1,9 @@ +export PATH=/opt/yunion/bin:$PATH + +test -f /etc/yunion/rcadmin && source /etc/yunion/rcadmin + +source <(climc --completion bash) + echo "Welcome to Cloud Shell :-) You may execute climc and other command tools in this shell." echo "Please exec 'climc' to get started" echo "" diff --git a/build/docker/Dockerfile.climc b/build/docker/Dockerfile.climc index 62fefd6160..4d4fb0b002 100644 --- a/build/docker/Dockerfile.climc +++ b/build/docker/Dockerfile.climc @@ -1,7 +1,7 @@ -FROM registry.cn-beijing.aliyuncs.com/yunionio/climc-base:20210901 +FROM registry.cn-beijing.aliyuncs.com/yunionio/climc-base:20230731.5 ADD ./build/climc/root/opt /opt -RUN cat /opt/yunion/scripts/motd/climc.sh >> /root/.bashrc +RUN cp /opt/yunion/scripts/motd/climc.sh /etc/profile.d/climc.sh && ln -sf /etc/profile.d/climc.sh /root/.bashrc ADD ./_output/alpine-build/bin/climc ./_output/alpine-build/bin/*cli /opt/yunion/bin/ diff --git a/build/docker/Dockerfile.climc-base b/build/docker/Dockerfile.climc-base index 989d45633b..0643f5e76b 100644 --- a/build/docker/Dockerfile.climc-base +++ b/build/docker/Dockerfile.climc-base @@ -1,5 +1,9 @@ FROM registry.cn-beijing.aliyuncs.com/yunionio/onecloud-base:v0.3.5 +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETARCH + MAINTAINER "Zexi Li " ENV TZ Asia/Shanghai @@ -8,11 +12,13 @@ RUN mkdir -p /opt/yunion/bin RUN echo http://dl-cdn.alpinelinux.org/alpine/edge/testing >>/etc/apk/repositories -RUN apk add --no-cache bash bash-completion tzdata ca-certificates vim ipmitool kubectl ceph-common && \ +RUN apk add --no-cache bash bash-completion tzdata ca-certificates vim ipmitool ceph-common dropbear shadow && \ rm -rf /var/cache/apk/* +RUN usermod --shell /bin/bash root + +RUN curl https://iso.yunion.cn/binaries/kubernetes-release/release/v1.22.9/bin/linux/${TARGETARCH}/kubectl -o /usr/bin/kubectl && chmod a+x /usr/bin/kubectl + RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime -ENV PATH="/opt/yunion/bin:${PATH}" - RUN mkdir -p /opt/yunion/bin diff --git a/build/docker/Dockerfile.webconsole b/build/docker/Dockerfile.webconsole index 41e773d56f..8eeac6111f 100644 --- a/build/docker/Dockerfile.webconsole +++ b/build/docker/Dockerfile.webconsole @@ -1,3 +1,3 @@ -FROM registry.cn-beijing.aliyuncs.com/yunionio/webconsole-base:v0.2-1 +FROM registry.cn-beijing.aliyuncs.com/yunionio/webconsole-base:20230731.2 ADD ./_output/alpine-build/bin/webconsole /opt/yunion/bin/webconsole diff --git a/build/docker/Makefile b/build/docker/Makefile index 2737e76c4c..4dd7c8bd68 100644 --- a/build/docker/Makefile +++ b/build/docker/Makefile @@ -20,13 +20,13 @@ ANSIBLESERVER_BASE = v1.1.1 ansibleserver-base: $(DOCKER_BUILDX)/ansibleserver-base:$(ANSIBLESERVER_BASE) -f ./Dockerfile.ansibleserver-base . -CLIMC_BASE_VERSION = 20210901 +CLIMC_BASE_VERSION = 20230731.5 climc-base: docker buildx build --platform linux/arm64,linux/amd64 --push \ -t registry.cn-beijing.aliyuncs.com/yunionio/climc-base:$(CLIMC_BASE_VERSION) -f ./Dockerfile.climc-base . -WEBCONSOLE_BASE_VERSION = v0.2-1 +WEBCONSOLE_BASE_VERSION = 20230731.2 webconsole-base: $(DOCKER_BUILDX)/webconsole-base:$(WEBCONSOLE_BASE_VERSION) -f ./Dockerfile.webconsole-base . diff --git a/build/docker/climc-entrypoint.sh b/build/docker/climc-entrypoint.sh new file mode 100644 index 0000000000..5a05d5e905 --- /dev/null +++ b/build/docker/climc-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +BASHRC=/root/.bashrc + +source $BASHRC + +climc sshkeypair-inject --admin + +mkdir -p /etc/dropbear + +mkdir -p /var/run/dropbear + +dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key + +dropbear -RFEjk -G root -p 22 diff --git a/cmd/climc/shell/compute/sshkeypairs.go b/cmd/climc/shell/compute/sshkeypairs.go index 810deb8de3..9fcfb3efa0 100644 --- a/cmd/climc/shell/compute/sshkeypairs.go +++ b/cmd/climc/shell/compute/sshkeypairs.go @@ -16,11 +16,17 @@ package compute import ( "fmt" + "os" + "path" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/hostman/guestfs/fsdriver" + deployapi "yunion.io/x/onecloud/pkg/hostman/hostdeployer/apis" "yunion.io/x/onecloud/pkg/mcclient" modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/util/procutils" ) func init() { @@ -28,7 +34,8 @@ func init() { Project string `help:"get keypair for specific project"` Admin bool `help:"get admin keypair, sysadmin ONLY option"` } - R(&SshkeypairQueryOptions{}, "sshkeypair-show", "Get ssh keypairs", func(s *mcclient.ClientSession, args *SshkeypairQueryOptions) error { + + getSshKeypair := func(s *mcclient.ClientSession, args *SshkeypairQueryOptions) (string, string, error) { query := jsonutils.NewDict() if args.Admin { query.Add(jsonutils.JSONTrue, "admin") @@ -37,22 +44,73 @@ func init() { if len(args.Project) == 0 { listResult, err := modules.Sshkeypairs.List(s, query) if err != nil { - return err + return "", "", err } keys = listResult.Data[0] } else { result, err := modules.Sshkeypairs.GetById(s, args.Project, query) if err != nil { - return err + return "", "", err } keys = result } privKey, _ := keys.GetString("private_key") pubKey, _ := keys.GetString("public_key") + return privKey, pubKey, nil + } + + R(&SshkeypairQueryOptions{}, "sshkeypair-show", "Get ssh keypairs", func(s *mcclient.ClientSession, args *SshkeypairQueryOptions) error { + privKey, pubKey, err := getSshKeypair(s, args) + if err != nil { + return err + } fmt.Print(privKey) fmt.Print(pubKey) return nil }) + + type SshkeypairInjectOptions struct { + SshkeypairQueryOptions + TargetDir string `help:"Target directory to save cloud ssh keypair"` + } + R(&SshkeypairInjectOptions{}, "sshkeypair-inject", "Inject ssh keypairs to local path", func(s *mcclient.ClientSession, args *SshkeypairInjectOptions) error { + _, pubKey, err := getSshKeypair(s, &args.SshkeypairQueryOptions) + if err != nil { + return err + } + targetDir := args.TargetDir + if targetDir == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return errors.Wrap(err, "get current user's home dir") + } + targetDir = homeDir + } + sshDir := path.Join(targetDir, ".ssh") + // MkdirAll anyways + os.MkdirAll(sshDir, 0700) + authFile := path.Join(sshDir, "authorized_keys") + var oldKeys string + + if procutils.NewCommand("test", "-f", authFile).Run() == nil { + output, err := procutils.NewCommand("cat", authFile).Output() + if err != nil { + return errors.Wrapf(err, "cat: %s", output) + } + oldKeys = string(output) + } + pubKeys := &deployapi.SSHKeys{AdminPublicKey: pubKey} + newKeys := fsdriver.MergeAuthorizedKeys(oldKeys, pubKeys) + if output, err := procutils.NewCommand( + "sh", "-c", fmt.Sprintf("echo '%s' > %s", newKeys, authFile)).Output(); err != nil { + return errors.Wrapf(err, "write public keys: %s", output) + } + if output, err := procutils.NewCommand( + "chmod", "0644", authFile).Output(); err != nil { + return errors.Wrapf(err, "chmod failed %s", output) + } + return nil + }) } diff --git a/pkg/mcclient/modules/webconsole/mod_webconsole.go b/pkg/mcclient/modules/webconsole/mod_webconsole.go index 817e5abe7f..367a90d038 100644 --- a/pkg/mcclient/modules/webconsole/mod_webconsole.go +++ b/pkg/mcclient/modules/webconsole/mod_webconsole.go @@ -81,7 +81,12 @@ func (m WebConsoleManager) DoCloudShell(s *mcclient.ClientSession, _ jsonutils.J return nil, errors.Wrap(err, "KubeClusters") } if len(clusters.Data) == 0 { - return nil, httperrors.NewNotFoundError("cluster system-default not found") + // maybe running in docker compose environment, so try to use ssh way + if data, err := m.DoClimcSshConnect(s, "climc", 22); err != nil { + return nil, httperrors.NewNotFoundError(errors.Wrap(err, "cluster system-default not found, try to use ssh way").Error()) + } else { + return data, nil + } } clusterId, _ := clusters.Data[0].GetString("id") if len(clusterId) == 0 { @@ -147,3 +152,16 @@ func (m WebConsoleManager) DoSshConnect(s *mcclient.ClientSession, id string, pa func (m WebConsoleManager) DoServerConnect(s *mcclient.ClientSession, id string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { return m.DoConnect(s, "server", id, "", params) } + +func (m WebConsoleManager) DoClimcSshConnect(s *mcclient.ClientSession, ip string, port int) (jsonutils.JSONObject, error) { + data := jsonutils.Marshal(map[string]interface{}{ + "username": "root", + "keep_username": true, + "ip_addr": ip, + "port": port, + "name": "climc", + }) + body := jsonutils.NewDict() + body.Set("webconsole", data) + return m.DoConnect(s, "climc", "shell", "", body) +} diff --git a/pkg/mcclient/options/webconsole.go b/pkg/mcclient/options/webconsole.go index 55d4ba8cf8..2f01c289ca 100644 --- a/pkg/mcclient/options/webconsole.go +++ b/pkg/mcclient/options/webconsole.go @@ -71,10 +71,11 @@ func (opt *WebConsoleBaremetalOptions) Params() (*jsonutils.JSONDict, error) { type WebConsoleSshOptions struct { WebConsoleOptions - IP string `help:"IP to connect" json:"-"` - Port int `help:"Remote server port"` - Username string `help:"Remote server username"` - Password string `help:"Remote server password"` + IP string `help:"IP to connect" json:"-"` + Port int `help:"Remote server port"` + Username string `help:"Remote server username"` + KeepUsername bool `help:"Keep remove username` + Password string `help:"Remote server password"` } func (opt *WebConsoleSshOptions) Params() (*jsonutils.JSONDict, error) { diff --git a/pkg/webconsole/command/climc_ssh_command.go b/pkg/webconsole/command/climc_ssh_command.go new file mode 100644 index 0000000000..e9cdd0ebf8 --- /dev/null +++ b/pkg/webconsole/command/climc_ssh_command.go @@ -0,0 +1,116 @@ +// Copyright 2019 Yunion +// +// 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. + +package command + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/webconsole/helper" +) + +type ClimcSshInfo struct { + IpAddr string `json:"ip_addr"` + Username string `json:"username"` +} + +type ClimcSshCommand struct { + *BaseCommand + Info *ClimcSshInfo + s *mcclient.ClientSession + keyFile string + buffer []byte +} + +func NewClimcSshCommand(info *ClimcSshInfo, s *mcclient.ClientSession) (*ClimcSshCommand, error) { + if info.IpAddr == "" { + return nil, fmt.Errorf("Empty host ip address") + } + if info.Username == "" { + return nil, fmt.Errorf("Empty username") + } + privateKey, err := helper.GetValidPrivateKey(info.IpAddr, 22, info.Username, "") + if err != nil { + return nil, errors.Wrap(err, "get cloud admin private key") + } + file, err := ioutil.TempFile("", fmt.Sprintf("id_rsa.%s.", info.IpAddr)) + if err != nil { + return nil, err + } + defer file.Close() + filename := file.Name() + { + err = os.Chmod(filename, 0600) + if err != nil { + return nil, err + } + _, err = file.Write([]byte(privateKey)) + if err != nil { + return nil, err + } + } + name := "bash" + args := []string{ + "-c", + fmt.Sprintf("ssh -o StrictHostKeyChecking=no -i %s %s@%s", filename, info.Username, info.IpAddr), + } + bCmd := NewBaseCommand(s, name, args...) + cmd := &ClimcSshCommand{ + BaseCommand: bCmd, + Info: info, + s: s, + keyFile: filename, + buffer: []byte{}, + } + return cmd, nil +} + +func (c ClimcSshCommand) GetCommand() *exec.Cmd { + cmd := c.BaseCommand.GetCommand() + cmd.Env = append(cmd.Env, "TERM=xterm-256color") + return cmd +} + +func (c ClimcSshCommand) GetProtocol() string { + return PROTOCOL_TTY +} + +func (c ClimcSshCommand) Cleanup() error { + if len(c.keyFile) > 0 { + os.Remove(c.keyFile) + c.keyFile = "" + } + return nil +} + +func (c *ClimcSshCommand) Scan(d byte, send func(msg string)) { + switch d { + case '\r': // 换行 + send("\r\n") + c.buffer = []byte{} + case '\u007f': // 退格 + if len(c.buffer) > 0 { + c.buffer = c.buffer[:len(c.buffer)-1] + send("\b \b") + } + default: + c.buffer = append(c.buffer, d) + } +} diff --git a/pkg/webconsole/command/socat_command.go b/pkg/webconsole/command/socat_command.go new file mode 100644 index 0000000000..a00cbebd3b --- /dev/null +++ b/pkg/webconsole/command/socat_command.go @@ -0,0 +1,63 @@ +// Copyright 2019 Yunion +// +// 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. + +package command + +import ( + "fmt" + "os/exec" + + "yunion.io/x/onecloud/pkg/mcclient" +) + +type SocatInfo struct { + IP string `json:"ip"` + Port int `json:"port"` +} + +type SocatCmd struct { + *BaseCommand + Info *SocatInfo + s *mcclient.ClientSession +} + +func NewSocatCommand(info *SocatInfo, s *mcclient.ClientSession) (*SocatCmd, error) { + if info.IP == "" { + return nil, fmt.Errorf("Empty remote ip address") + } + if info.Port <= 0 { + return nil, fmt.Errorf("Invalid port %d", info.Port) + } + + name := "bash" + args := []string{ + "-c", + fmt.Sprintf("socat FILE:`tty`,raw,echo=0 TCP:%s:%d", info.IP, info.Port), + } + cmd := NewBaseCommand(s, name, args...) + scmd := &SocatCmd{ + BaseCommand: cmd, + Info: info, + } + + return scmd, nil +} + +func (c *SocatCmd) GetCommand() *exec.Cmd { + return c.BaseCommand.GetCommand() +} + +func (c *SocatCmd) GetProtocol() string { + return PROTOCOL_TTY +} diff --git a/pkg/webconsole/handlers.go b/pkg/webconsole/handlers.go index 54c9bf7d07..9c3c2de091 100644 --- a/pkg/webconsole/handlers.go +++ b/pkg/webconsole/handlers.go @@ -51,6 +51,7 @@ const ( func InitHandlers(app *appsrv.Application) { app.AddHandler("POST", ApiPathPrefix+"k8s//shell", auth.Authenticate(handleK8sShell)) + app.AddHandler("POST", ApiPathPrefix+"climc/shell", auth.Authenticate(handleClimcShell)) 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)) @@ -170,9 +171,10 @@ func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) ip := env.Params[""] port, _ := env.Body.Int("port") username, _ := env.Body.GetString("username") + keepusername, _ := env.Body.Bool("keep_username") password, _ := env.Body.GetString("password") name, _ := env.Body.GetString("name") - s := session.NewSshSession(ctx, env.ClientSessin, name, ip, port, username, password) + s := session.NewSshSession(ctx, env.ClientSessin, name, ip, port, username, password, keepusername) handleSshSession(ctx, s, w) } @@ -206,6 +208,26 @@ func handleBaremetalShell(ctx context.Context, w http.ResponseWriter, r *http.Re handleCommandSession(ctx, cmd, w) } +func handleClimcShell(ctx context.Context, w http.ResponseWriter, r *http.Request) { + env, err := fetchCloudEnv(ctx, w, r) + if err != nil { + httperrors.GeneralServerError(ctx, w, err) + return + } + info := command.ClimcSshInfo{} + err = env.Body.Unmarshal(&info) + if err != nil { + httperrors.GeneralServerError(ctx, w, err) + return + } + cmd, err := command.NewClimcSshCommand(&info, env.ClientSessin) + if err != nil { + httperrors.GeneralServerError(ctx, w, err) + return + } + handleCommandSession(ctx, cmd, w) +} + func handleServerRemoteConsole(ctx context.Context, w http.ResponseWriter, r *http.Request) { env, err := fetchCloudEnv(ctx, w, r) if err != nil { diff --git a/pkg/webconsole/helper/doc.go b/pkg/webconsole/helper/doc.go new file mode 100644 index 0000000000..1fe8dbea3f --- /dev/null +++ b/pkg/webconsole/helper/doc.go @@ -0,0 +1 @@ +package helper // import "yunion.io/x/onecloud/pkg/webconsole/helper" diff --git a/pkg/webconsole/helper/helper.go b/pkg/webconsole/helper/helper.go new file mode 100644 index 0000000000..01a6c5e499 --- /dev/null +++ b/pkg/webconsole/helper/helper.go @@ -0,0 +1,95 @@ +// Copyright 2019 Yunion +// +// 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. + +package helper + +import ( + "context" + "fmt" + "time" + + "golang.org/x/crypto/ssh" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + o "yunion.io/x/onecloud/pkg/webconsole/options" +) + +func GetValidPrivateKey(host string, port int64, username string, projectId string) (string, error) { + errs := []error{} + ctx := context.Background() + admin := auth.GetAdminSession(ctx, o.Options.Region) + for _, gf := range []func() (jsonutils.JSONObject, error){ + func() (jsonutils.JSONObject, error) { + if projectId == "" { + return nil, errors.Error("project_id is empty") + } + key, err := compute.Sshkeypairs.GetById(admin, projectId, jsonutils.Marshal(map[string]bool{"admin": true})) + if err != nil { + return nil, errors.Wrapf(err, "Sshkeypairs.GetById(%s)", projectId) + } + return key, nil + }, + func() (jsonutils.JSONObject, error) { + query := jsonutils.NewDict() + query.Set("admin", jsonutils.JSONTrue) + ret, err := compute.Sshkeypairs.List(admin, query) + if err != nil { + return nil, errors.Wrap(err, "modules.Sshkeypairs.List") + } + if len(ret.Data) == 0 { + return nil, errors.Wrap(httperrors.ErrNotFound, "Not found admin sshkey") + } + keys := ret.Data[0] + return keys, nil + }, + } { + key, err := gf() + if err != nil { + errs = append(errs, err) + continue + } + privKey, err := key.GetString("private_key") + if err != nil { + errs = append(errs, errors.Wrapf(err, "get private_key")) + continue + } + signer, err := ssh.ParsePrivateKey([]byte(privKey)) + if err != nil { + errs = append(errs, errors.Wrapf(err, "ParsePrivateKey")) + continue + } + config := &ssh.ClientConfig{ + Timeout: time.Second, + User: username, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + } + addr := fmt.Sprintf("%s:%d", host, port) + client, err := ssh.Dial("tcp", addr, config) + if err != nil { + errs = append(errs, errors.Wrapf(err, "dial %s", addr)) + continue + } + defer client.Close() + return privKey, nil + } + return "", errors.NewAggregate(errs) +} diff --git a/pkg/webconsole/session/session.go b/pkg/webconsole/session/session.go index 2284eea2fa..eca8c42d49 100644 --- a/pkg/webconsole/session/session.go +++ b/pkg/webconsole/session/session.go @@ -133,7 +133,7 @@ type SSession struct { recorder recorder.Recoder } -func (s SSession) GetConnectParams(params url.Values) (string, error) { +func (s *SSession) GetConnectParams(params url.Values) (string, error) { if params == nil { params = url.Values(make(map[string][]string)) } diff --git a/pkg/webconsole/session/ssh_session.go b/pkg/webconsole/session/ssh_session.go index 9c4628c15f..569063a575 100644 --- a/pkg/webconsole/session/ssh_session.go +++ b/pkg/webconsole/session/ssh_session.go @@ -29,8 +29,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/webconsole" "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/mcclient/auth" - "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + "yunion.io/x/onecloud/pkg/webconsole/helper" o "yunion.io/x/onecloud/pkg/webconsole/options" "yunion.io/x/onecloud/pkg/webconsole/recorder" ) @@ -46,18 +45,22 @@ type SSshSession struct { Port int64 PrivateKey string Username string - Password string + // 保持原有 Username ,不实用 cloudroot 的同时使用 PrivateKey + KeepUsername bool + Password string } -func NewSshSession(ctx context.Context, us *mcclient.ClientSession, name, ip string, port int64, username, password string) *SSshSession { +func NewSshSession(ctx context.Context, us *mcclient.ClientSession, + name, ip string, port int64, username, password string, KeepUsername bool) *SSshSession { ret := &SSshSession{ - us: us, - id: stringutils.UUID4(), - Port: port, - Host: ip, - name: name, - Username: username, - Password: password, + us: us, + id: stringutils.UUID4(), + Port: port, + Host: ip, + name: name, + Username: username, + KeepUsername: KeepUsername, + Password: password, } if port <= 0 { ret.Port = 22 @@ -113,41 +116,17 @@ func (s *SSshSession) IsNeedLogin() (bool, error) { if !o.Options.EnableAutoLogin { return true, nil } - privateKey, err := func() (string, error) { - ctx := context.Background() - admin := auth.GetAdminSession(ctx, o.Options.Region) - key, err := compute.Sshkeypairs.GetById(admin, s.us.GetProjectId(), jsonutils.Marshal(map[string]bool{"admin": true})) - if err != nil { - return "", errors.Wrapf(err, "Sshkeypairs.GetById(%s)", s.us.GetProjectId()) + if !s.KeepUsername { + s.Username = "cloudroot" + } else { + if s.Username == "" { + return true, errors.Error("username is empty") } - privKey, err := key.GetString("private_key") - if err != nil { - return "", errors.Wrapf(err, "get private_key") - } - signer, err := ssh.ParsePrivateKey([]byte(privKey)) - if err != nil { - return "", errors.Wrapf(err, "ParsePrivateKey") - } - config := &ssh.ClientConfig{ - Timeout: time.Second, - User: "cloudroot", - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(signer), - }, - } - addr := fmt.Sprintf("%s:%d", s.Host, s.Port) - client, err := ssh.Dial("tcp", addr, config) - if err != nil { - return "", errors.Wrapf(err, "dial %s", addr) - } - defer client.Close() - return privKey, nil - }() - if err != nil { - return true, err } - s.Username = "cloudroot" + privateKey, err := helper.GetValidPrivateKey(s.Host, s.Port, s.Username, s.us.GetProjectId()) + if err != nil { + return true, errors.Wrap(err, "try to use cloud admin private_key for ssh login") + } s.PrivateKey = privateKey return false, nil }