Merge pull request #18998 from swordqiu/automated-cherry-pick-of-#18997-upstream-release-3.11

Automated cherry pick of #18997: fix: refine webconsole ssh api
This commit is contained in:
Zexi Li
2023-12-15 11:50:43 +08:00
committed by GitHub
10 changed files with 272 additions and 37 deletions
+1 -1
View File
@@ -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
}
+5 -1
View File
@@ -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) {
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -37,7 +37,7 @@ import (
type WebsocketServer struct {
Session *session.SSession
Host string
Port int64
Port int
Username string
Password string
PrivateKey string
@@ -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/<podName>/shell", auth.Authenticate(handleK8sShell))
app.AddHandler("POST", ApiPathPrefix+"climc/shell", auth.Authenticate(handleClimcShell))
app.AddHandler("POST", ApiPathPrefix+"k8s/<podName>/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["<ip>"]
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["<ip>"]
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["<id>"]
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
+5 -6
View File
@@ -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)
+15 -5
View File
@@ -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
}
+205
View File
@@ -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
}
+9 -10
View File
@@ -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