diff --git a/cmd/climc/shell/compute/webconsole.go b/cmd/climc/shell/compute/webconsole.go index b246cdcd69..a80de28621 100644 --- a/cmd/climc/shell/compute/webconsole.go +++ b/cmd/climc/shell/compute/webconsole.go @@ -117,7 +117,7 @@ func init() { if err != nil { return err } - ret, err := webconsole.WebConsole.DoSshConnect(s, args.IP, params) + ret, err := webconsole.WebConsole.DoSshConnect(s, args.ID, params) if err != nil { return err } diff --git a/pkg/cloudcommon/db/fetch.go b/pkg/cloudcommon/db/fetch.go index 861bd7d435..48d8817768 100644 --- a/pkg/cloudcommon/db/fetch.go +++ b/pkg/cloudcommon/db/fetch.go @@ -406,7 +406,11 @@ func FetchCheckQueryOwnerScope( ownerId = userCred reqScopeStr, _ := data.GetString("scope") if len(reqScopeStr) > 0 { - queryScope = rbacscope.String2Scope(reqScopeStr) + if reqScopeStr == "max" || reqScopeStr == "maxallowed" { + queryScope = allowScope + } else { + queryScope = rbacscope.String2Scope(reqScopeStr) + } } else if data.Contains("admin") { isAdmin := jsonutils.QueryBoolean(data, "admin", false) if isAdmin && allowScope.HigherThan(rbacscope.ScopeProject) { diff --git a/pkg/mcclient/options/webconsole.go b/pkg/mcclient/options/webconsole.go index 14aa8c211d..ec9e8c8c7f 100644 --- a/pkg/mcclient/options/webconsole.go +++ b/pkg/mcclient/options/webconsole.go @@ -71,7 +71,8 @@ func (opt *WebConsoleBaremetalOptions) Params() (*jsonutils.JSONDict, error) { type WebConsoleSshOptions struct { WebConsoleOptions - IP string `help:"IP to connect" json:"-"` + ID string `help:"ID of server" json:"-"` + Ip string `help:"IP to connect if multiple IPs on server"` Port int `help:"Remote server port"` Username string `help:"Remote server username"` KeepUsername bool `help:"Keep remove username` @@ -97,7 +98,7 @@ type WebConsoleServerRdpOptions struct { WebConsoleOptions ID string `help:"Server id or name"` - HOST string + Host *string Port *int Username *string Password *string diff --git a/pkg/webconsole/helper/helper.go b/pkg/webconsole/helper/helper.go index 008044c5db..3e5049245a 100644 --- a/pkg/webconsole/helper/helper.go +++ b/pkg/webconsole/helper/helper.go @@ -30,7 +30,7 @@ import ( o "yunion.io/x/onecloud/pkg/webconsole/options" ) -func GetValidPrivateKey(host string, port int64, username string, projectId string) (string, error) { +func GetValidPrivateKey(host string, port int, username string, projectId string) (string, error) { errs := []error{} ctx := context.Background() admin := auth.GetAdminSession(ctx, o.Options.Region) diff --git a/pkg/webconsole/server/ssh_server.go b/pkg/webconsole/server/ssh_server.go index 28fba1d455..dcc11fdbe5 100644 --- a/pkg/webconsole/server/ssh_server.go +++ b/pkg/webconsole/server/ssh_server.go @@ -37,7 +37,7 @@ import ( type WebsocketServer struct { Session *session.SSession Host string - Port int64 + Port int Username string Password string PrivateKey string diff --git a/pkg/webconsole/handlers.go b/pkg/webconsole/service/handlers.go similarity index 91% rename from pkg/webconsole/handlers.go rename to pkg/webconsole/service/handlers.go index 2d5f34634a..a035c693da 100644 --- a/pkg/webconsole/handlers.go +++ b/pkg/webconsole/service/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package webconsole +package service import ( "context" @@ -25,6 +25,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" + "yunion.io/x/pkg/util/regutils" webconsole_api "yunion.io/x/onecloud/pkg/apis/webconsole" "yunion.io/x/onecloud/pkg/appsrv" @@ -50,7 +51,7 @@ const ( WebsocketProxyPathPrefix = "/wsproxy/" ) -func InitHandlers(app *appsrv.Application) { +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)) @@ -173,13 +174,29 @@ func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) httperrors.GeneralServerError(ctx, w, err) return } - 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, keepusername) + + sshConnInfo := session.SSshConnectionInfo{} + if !gotypes.IsNil(env.Body) { + err = env.Body.Unmarshal(&sshConnInfo) + if err != nil { + httperrors.InputParameterError(ctx, w, "unmarshal error: %s", err.Error()) + return + } + } + idStr := env.Params[""] + if !regutils.MatchIPAddr(idStr) { + ip, port, err := session.ResolveServerSSHIPPortById(ctx, env.ClientSessin, idStr, sshConnInfo.IP, sshConnInfo.Port) + if err != nil { + httperrors.GeneralServerError(ctx, w, err) + return + } + sshConnInfo.IP = ip + sshConnInfo.Port = port + } else { + // directly ssh IP should be deprecated gradually + sshConnInfo.IP = idStr + } + s := session.NewSshSession(ctx, env.ClientSessin, sshConnInfo) handleSshSession(ctx, s, w) } @@ -267,7 +284,7 @@ func handleServerRemoteRDPConsole(ctx context.Context, w http.ResponseWriter, r } query := env.Body srvId := env.Params[""] - info, err := session.NewRemoteRDPConsoleInfoByCloud(env.ClientSessin, srvId, query) + info, err := session.NewRemoteRDPConsoleInfoByCloud(ctx, env.ClientSessin, srvId, query) if err != nil { httperrors.GeneralServerError(ctx, w, err) return diff --git a/pkg/webconsole/service/service.go b/pkg/webconsole/service/service.go index 50cf938af7..1ed7c25952 100644 --- a/pkg/webconsole/service/service.go +++ b/pkg/webconsole/service/service.go @@ -36,7 +36,6 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/cronman" "yunion.io/x/onecloud/pkg/cloudcommon/db" common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" - "yunion.io/x/onecloud/pkg/webconsole" "yunion.io/x/onecloud/pkg/webconsole/models" o "yunion.io/x/onecloud/pkg/webconsole/options" "yunion.io/x/onecloud/pkg/webconsole/server" @@ -90,7 +89,7 @@ func start() { cloudcommon.InitDB(dbOpts) - webconsole.InitHandlers(app) + initHandlers(app) db.EnsureAppSyncDB(app, dbOpts, models.InitDB) @@ -98,17 +97,17 @@ func start() { root.UseEncodedPath() // api handler - root.PathPrefix(webconsole.ApiPathPrefix).Handler(app) + root.PathPrefix(ApiPathPrefix).Handler(app) srv := server.NewConnectionServer() // websocket command text console handler - root.Handle(webconsole.ConnectPathPrefix, srv) + root.Handle(ConnectPathPrefix, srv) // websockify graphic console handler - root.Handle(webconsole.WebsockifyPathPrefix, srv) + root.Handle(WebsockifyPathPrefix, srv) // websocketproxy handler - root.Handle(webconsole.WebsocketProxyPathPrefix, srv) + root.Handle(WebsocketProxyPathPrefix, srv) // misc handler addMiscHandlers(app, root) diff --git a/pkg/webconsole/session/remote_console_rdp.go b/pkg/webconsole/session/remote_console_rdp.go index 82425ad83b..83adf0514f 100644 --- a/pkg/webconsole/session/remote_console_rdp.go +++ b/pkg/webconsole/session/remote_console_rdp.go @@ -15,9 +15,11 @@ package session import ( + "context" "os/exec" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/gotypes" api "yunion.io/x/onecloud/pkg/apis/webconsole" @@ -40,10 +42,21 @@ type RemoteRDPConsoleInfo struct { s *mcclient.ClientSession } -func NewRemoteRDPConsoleInfoByCloud(s *mcclient.ClientSession, serverId string, query jsonutils.JSONObject) (*RemoteRDPConsoleInfo, error) { +func NewRemoteRDPConsoleInfoByCloud(ctx context.Context, s *mcclient.ClientSession, serverId string, query jsonutils.JSONObject) (*RemoteRDPConsoleInfo, error) { info := &RemoteRDPConsoleInfo{s: s} if !gotypes.IsNil(query) { - query.Unmarshal(&info) + err := query.Unmarshal(&info) + if err != nil { + return nil, errors.Wrap(err, "Unmarshal") + } + } + if info.Port <= 0 { + info.Port = 3389 + } + var err error + info.Host, info.Port, err = resolveServerIPPortById(ctx, s, serverId, info.Host, info.Port) + if err != nil { + return nil, errors.Wrap(err, "resolveServerIPPortById") } if len(info.Host) == 0 { return nil, httperrors.NewMissingParameterError("host") @@ -59,9 +72,6 @@ func NewRemoteRDPConsoleInfoByCloud(s *mcclient.ClientSession, serverId string, info.Password, _ = ret.GetString("password") info.Username, _ = ret.GetString("username") } - if info.Port == 0 { - info.Port = 3389 - } return info, nil } diff --git a/pkg/webconsole/session/resolve_sshinfo.go b/pkg/webconsole/session/resolve_sshinfo.go new file mode 100644 index 0000000000..39b6bf3bd1 --- /dev/null +++ b/pkg/webconsole/session/resolve_sshinfo.go @@ -0,0 +1,205 @@ +// 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 session + +import ( + "context" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + compute_api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute" + options "yunion.io/x/onecloud/pkg/mcclient/options/compute" +) + +type SSshConnectionInfo struct { + IP string `json:"ip"` + Port int `json:"port"` + Username string `json:"username"` + KeepUsername bool `json:"keep_username"` + Password string `json:"password"` + Name string `json:"name"` +} + +func ResolveServerSSHIPPortById(ctx context.Context, s *mcclient.ClientSession, id string, ip string, port int) (string, int, error) { + if port <= 0 { + port = 22 + } + return resolveServerIPPortById(ctx, s, id, ip, port) +} + +func resolveServerIPPortById(ctx context.Context, s *mcclient.ClientSession, id string, ip string, port int) (string, int, error) { + guestInfo, err := compute.Servers.Get(s, id, nil) + if err != nil { + return "", 0, errors.Wrapf(err, "GetById %s", id) + } + guestDetails := compute_api.SGuest{} + err = guestInfo.Unmarshal(&guestDetails) + if err != nil { + return "", 0, errors.Wrap(err, "Unmarshal guest info") + } + // list all nic of a server + input := compute_api.GuestnetworkListInput{} + input.ServerId = guestDetails.Id + True := true + input.Details = &True + input.ServerFilterListInput.Scope = "max" + result, err := compute.Servernetworks.List(s, jsonutils.Marshal(input)) + if err != nil { + return "", 0, errors.Wrap(err, "Servernetworks.List") + } + if result.Total == 0 { + // not nic found!!! + return "", 0, errors.Wrap(httperrors.ErrNotFound, "no nic on server") + } + + // find nics + var guestNicDetails compute_api.GuestnetworkDetails + if result.Total == 1 { + err := result.Data[0].Unmarshal(&guestNicDetails) + if err != nil { + return "", 0, errors.Wrap(err, "Unmarshal guest network info") + } + if len(ip) > 0 && ip != guestNicDetails.EipAddr && ip != guestNicDetails.IpAddr && ip != guestNicDetails.Ip6Addr { + return "", 0, errors.Wrapf(httperrors.ErrInputParameter, "ip %s not match with server", ip) + } + } else { + if len(ip) == 0 { + return "", 0, errors.Wrap(httperrors.ErrInputParameter, "must specify ip") + } + find := false + for _, gnJson := range result.Data { + err := gnJson.Unmarshal(&guestNicDetails) + if err != nil { + return "", 0, errors.Wrap(err, "Unmarshal guest network info") + } + if ip == guestNicDetails.EipAddr || ip == guestNicDetails.IpAddr || ip == guestNicDetails.Ip6Addr { + find = true + break + } + } + if !find { + return "", 0, errors.Wrap(httperrors.ErrInputParameter, "ip specified not match with server") + } + } + + if len(ip) == 0 { + // guest ip + if len(guestNicDetails.EipAddr) > 0 { + ip = guestNicDetails.EipAddr + } else if len(guestNicDetails.IpAddr) > 0 { + ip = guestNicDetails.IpAddr + } else { + return "", 0, errors.Wrap(httperrors.ErrNotSupported, "no valid ipv4 addr") + } + } + + if ip == guestNicDetails.Ip6Addr { + return "", 0, errors.Wrap(httperrors.ErrNotSupported, "ipv6 not supported") + } + + if ip == guestNicDetails.IpAddr && len(guestNicDetails.MappedIpAddr) > 0 { + // need to do open forward + return acquireForward(ctx, s, guestDetails.Id, ip, "tcp", port) + } + + return ip, port, nil +} + +type sForwardInfo struct { + ProxyAddr string `json:"proxy_addr"` + ProxyPort int `json:"proxy_port"` +} + +func acquireForward(ctx context.Context, session *mcclient.ClientSession, srvid string, ip string, proto string, port int) (string, int, error) { + lockman.LockRawObject(ctx, "server", srvid) + defer lockman.ReleaseRawObject(ctx, "server", srvid) + + addr, port, err := listForward(session, srvid, ip, proto, port) + if err == nil { + return addr, port, nil + } + if errors.Cause(err) == httperrors.ErrNotFound { + return openForward(session, srvid, ip, proto, port) + } else { + return "", 0, errors.Wrap(err, "listForward") + } +} + +func listForward(session *mcclient.ClientSession, srvid string, ip string, proto string, port int) (string, int, error) { + opt := &options.ServerListForwardOptions{ + ServerIdOptions: options.ServerIdOptions{ + ID: srvid, + }, + Proto: &proto, + Port: &port, + Addr: &ip, + } + + params, err := opt.Params() + if err != nil { + return "", 0, errors.Wrap(err, "get list forward params") + } + jsonItem, err := modules.Servers.PerformAction(session, opt.ID, "list-forward", params) + if err != nil { + return "", 0, errors.Wrap(err, "list-forward") + } + + infoList := make([]sForwardInfo, 0) + err = jsonItem.Unmarshal(&infoList, "forwards") + if err != nil { + return "", 0, errors.Wrap(err, "Unmarshal forwards") + } + + if len(infoList) > 0 { + return infoList[0].ProxyAddr, infoList[0].ProxyPort, nil + } + + return "", 0, errors.Wrap(httperrors.ErrNotFound, "no forwards") +} + +func openForward(session *mcclient.ClientSession, srvid string, ip string, proto string, port int) (string, int, error) { + opt := &options.ServerOpenForwardOptions{ + ServerIdOptions: options.ServerIdOptions{ + ID: srvid, + }, + Proto: proto, + Port: port, + Addr: ip, + } + + params, err := opt.Params() + if err != nil { + return "", 0, errors.Wrap(err, "get open forward params") + } + + jsonItem, err := modules.Servers.PerformAction(session, opt.ID, "open-forward", params) + if err != nil { + return "", 0, errors.Wrap(err, "open-forward") + } + + info := sForwardInfo{} + err = jsonItem.Unmarshal(&info) + if err != nil { + return "", 0, errors.Wrap(err, "Unmarshal") + } + + return info.ProxyAddr, info.ProxyPort, nil +} diff --git a/pkg/webconsole/session/ssh_session.go b/pkg/webconsole/session/ssh_session.go index 569063a575..744576a4a9 100644 --- a/pkg/webconsole/session/ssh_session.go +++ b/pkg/webconsole/session/ssh_session.go @@ -42,7 +42,7 @@ type SSshSession struct { name string Host string - Port int64 + Port int PrivateKey string Username string // 保持原有 Username ,不实用 cloudroot 的同时使用 PrivateKey @@ -50,19 +50,18 @@ type SSshSession struct { Password string } -func NewSshSession(ctx context.Context, us *mcclient.ClientSession, - name, ip string, port int64, username, password string, KeepUsername bool) *SSshSession { +func NewSshSession(ctx context.Context, us *mcclient.ClientSession, conn SSshConnectionInfo) *SSshSession { ret := &SSshSession{ us: us, id: stringutils.UUID4(), - Port: port, - Host: ip, - name: name, - Username: username, - KeepUsername: KeepUsername, - Password: password, + Port: conn.Port, + Host: conn.IP, + name: conn.Name, + Username: conn.Username, + KeepUsername: conn.KeepUsername, + Password: conn.Password, } - if port <= 0 { + if conn.Port <= 0 { ret.Port = 22 } return ret