Merge pull request #17682 from zexi/automated-cherry-pick-of-#17681-upstream-master

Automated cherry pick of #17681: fix(webconsole,climc): cloud shell within docker compose environment
This commit is contained in:
Zexi Li
2023-08-01 00:48:08 +08:00
committed by GitHub
17 changed files with 463 additions and 62 deletions
+19
View File
@@ -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
@@ -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 ""
+2 -2
View File
@@ -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/
+9 -3
View File
@@ -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 <lizexi@yunionyun.com>"
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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 .
+17
View File
@@ -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
+61 -3
View File
@@ -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
})
}
@@ -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)
}
+5 -4
View File
@@ -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) {
+116
View File
@@ -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)
}
}
+63
View File
@@ -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
}
+23 -1
View File
@@ -51,6 +51,7 @@ const (
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))
app.AddHandler("POST", ApiPathPrefix+"baremetal/<id>", auth.Authenticate(handleBaremetalShell))
app.AddHandler("POST", ApiPathPrefix+"ssh/<ip>", auth.Authenticate(handleSshShell))
@@ -170,9 +171,10 @@ func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request)
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)
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 {
+1
View File
@@ -0,0 +1 @@
package helper // import "yunion.io/x/onecloud/pkg/webconsole/helper"
+95
View File
@@ -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)
}
+1 -1
View File
@@ -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))
}
+23 -44
View File
@@ -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
}