mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
Merge pull request #14072 from zexi/webconsole-ssh-log
feat(webconsole): ssh command log
This commit is contained in:
@@ -36,6 +36,7 @@ import (
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/quota"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/scheduledtask"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/scheduler"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/webconsole"
|
||||
_ "yunion.io/x/onecloud/cmd/climc/shell/yunionconf"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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 webconsole
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
|
||||
"yunion.io/x/onecloud/cmd/climc/shell"
|
||||
"yunion.io/x/onecloud/cmd/climc/shell/events"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules/webconsole"
|
||||
)
|
||||
|
||||
var (
|
||||
R = shell.R
|
||||
)
|
||||
|
||||
func init() {
|
||||
R(&events.EventListOptions{}, "webconsole-commandlog", "Show webconsole command logs", func(s *mcclient.ClientSession, args *events.EventListOptions) {
|
||||
ret, err := webconsole.CommandLog.List(s, jsonutils.Marshal(args))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
shell.PrintList(ret, webconsole.CommandLog.GetColumns(s))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 webconsole
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
)
|
||||
|
||||
type CommandLogListInput struct {
|
||||
apis.Meta
|
||||
apis.OpsLogListInput
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 webconsole
|
||||
|
||||
import (
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||||
)
|
||||
|
||||
var (
|
||||
CommandLog *CommandLogManager
|
||||
)
|
||||
|
||||
func init() {
|
||||
CommandLog = NewCommandLogManager()
|
||||
|
||||
modulebase.Register("v1", CommandLog)
|
||||
}
|
||||
|
||||
type CommandLogManager struct {
|
||||
modulebase.ResourceManager
|
||||
}
|
||||
|
||||
func NewCommandLogManager() *CommandLogManager {
|
||||
return &CommandLogManager{
|
||||
modulebase.ResourceManager{
|
||||
BaseManager: *modulebase.NewBaseManager("webconsole", "", "webconsole", []string{
|
||||
"id", "ops_time", "obj_id", "obj_type", "obj_name", "user", "user_id", "tenant", "tenant_id", "owner_tenant_id", "action", "notes",
|
||||
"session_id", "accessed_at", "type", "start_time", "command",
|
||||
}, nil),
|
||||
Keyword: "commandlog", KeywordPlural: "commandlogs",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/recorder"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,20 +36,28 @@ type ICommand interface {
|
||||
IsNeedShowInfo() bool
|
||||
ShowInfo() string
|
||||
Scan(d byte, send func(msg string))
|
||||
GetClientSession() *mcclient.ClientSession
|
||||
GetRecordObject() *recorder.Object
|
||||
}
|
||||
|
||||
type BaseCommand struct {
|
||||
s *mcclient.ClientSession
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
|
||||
func NewBaseCommand(name string, args ...string) *BaseCommand {
|
||||
func NewBaseCommand(s *mcclient.ClientSession, name string, args ...string) *BaseCommand {
|
||||
return &BaseCommand{
|
||||
s: s,
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BaseCommand) GetClientSession() *mcclient.ClientSession {
|
||||
return c.s
|
||||
}
|
||||
|
||||
func (c *BaseCommand) AppendArgs(args ...string) *BaseCommand {
|
||||
for _, arg := range args {
|
||||
c.args = append(c.args, arg)
|
||||
@@ -59,7 +70,6 @@ func (c BaseCommand) GetCommand() *exec.Cmd {
|
||||
}
|
||||
|
||||
func (c BaseCommand) Scan(byte, func(msg string)) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c BaseCommand) IsNeedShowInfo() bool {
|
||||
@@ -71,10 +81,13 @@ func (c BaseCommand) ShowInfo() string {
|
||||
}
|
||||
|
||||
func (c BaseCommand) Reconnect() {
|
||||
return
|
||||
}
|
||||
|
||||
func (c BaseCommand) Cleanup() error {
|
||||
log.Infof("BaseCommand Cleanup do nothing")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BaseCommand) GetRecordObject() *recorder.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
o "yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
)
|
||||
|
||||
@@ -33,9 +34,10 @@ type IpmiInfo struct {
|
||||
type IpmitoolSol struct {
|
||||
*BaseCommand
|
||||
Info *IpmiInfo
|
||||
s *mcclient.ClientSession
|
||||
}
|
||||
|
||||
func NewIpmitoolSolCommand(info *IpmiInfo) (*IpmitoolSol, error) {
|
||||
func NewIpmitoolSolCommand(info *IpmiInfo, s *mcclient.ClientSession) (*IpmitoolSol, error) {
|
||||
if info.IpAddr == "" {
|
||||
return nil, fmt.Errorf("Empty host ip address")
|
||||
}
|
||||
@@ -53,7 +55,7 @@ func NewIpmitoolSolCommand(info *IpmiInfo) (*IpmitoolSol, error) {
|
||||
"-P", info.Password,
|
||||
"sol",
|
||||
}
|
||||
cmd := NewBaseCommand(name, solArgs...)
|
||||
cmd := NewBaseCommand(s, name, solArgs...)
|
||||
cmd.AppendArgs("activate")
|
||||
tool := &IpmitoolSol{
|
||||
BaseCommand: cmd,
|
||||
|
||||
@@ -24,10 +24,12 @@ import (
|
||||
"yunion.io/x/log"
|
||||
|
||||
webconsole_api "yunion.io/x/onecloud/pkg/apis/webconsole"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
o "yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
)
|
||||
|
||||
type K8sEnv struct {
|
||||
Session *mcclient.ClientSession
|
||||
Cluster string
|
||||
Namespace string
|
||||
Pod string
|
||||
@@ -41,12 +43,12 @@ type Kubectl struct {
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
func NewKubectlCommand(kubeconfig, namespace string) *Kubectl {
|
||||
func NewKubectlCommand(s *mcclient.ClientSession, kubeconfig, namespace string) *Kubectl {
|
||||
name := o.Options.KubectlPath
|
||||
if len(namespace) == 0 {
|
||||
namespace = "default"
|
||||
}
|
||||
cmd := NewBaseCommand(name, "--namespace", namespace)
|
||||
cmd := NewBaseCommand(s, name, "--namespace", namespace)
|
||||
return &Kubectl{
|
||||
BaseCommand: cmd,
|
||||
kubeconfig: kubeconfig,
|
||||
@@ -132,7 +134,7 @@ func NewPodBashCommand(env *K8sEnv) ICommand {
|
||||
shellRequest.Command = "env"
|
||||
}
|
||||
|
||||
return NewKubectlCommand(env.Kubeconfig, env.Namespace).Exec().
|
||||
return NewKubectlCommand(env.Session, env.Kubeconfig, env.Namespace).Exec().
|
||||
Stdin().
|
||||
TTY().
|
||||
Pod(env.Pod).
|
||||
@@ -189,7 +191,7 @@ func (c *KubectlLog) Since(data jsonutils.JSONObject) *KubectlLog {
|
||||
}
|
||||
|
||||
func NewPodLogCommand(env *K8sEnv) ICommand {
|
||||
return NewKubectlCommand(env.Kubeconfig, env.Namespace).Logs().
|
||||
return NewKubectlCommand(env.Session, env.Kubeconfig, env.Namespace).Logs().
|
||||
Follow().
|
||||
Pod(env.Pod).
|
||||
Since(env.Data).
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestKubectlExec_Command(t *testing.T) {
|
||||
{
|
||||
name: "bash command",
|
||||
fields: fields{
|
||||
Kubectl: NewKubectlCommand("/tmp/kubeconfig", "system"),
|
||||
Kubectl: NewKubectlCommand(nil, "/tmp/kubeconfig", "system"),
|
||||
},
|
||||
args: args{
|
||||
cmd: "bash",
|
||||
|
||||
@@ -25,13 +25,16 @@ import (
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"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/util/ansible"
|
||||
"yunion.io/x/onecloud/pkg/util/ssh"
|
||||
o "yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/recorder"
|
||||
)
|
||||
|
||||
type SSHtoolSol struct {
|
||||
@@ -45,14 +48,28 @@ type SSHtoolSol struct {
|
||||
keyFile string
|
||||
buffer []byte
|
||||
needShowInfo bool
|
||||
objectType string
|
||||
object jsonutils.JSONObject
|
||||
}
|
||||
|
||||
func getCommand(ctx context.Context, userCred mcclient.TokenCredential, ip string, port int) (string, *BaseCommand, error) {
|
||||
func getObjectFromRemote(us *mcclient.ClientSession, id string, objType string) (jsonutils.JSONObject, error) {
|
||||
gFunc := func(_ *mcclient.ClientSession, _ string, _ jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
return nil, errors.Errorf("Can't get object %q by type %q", id, objType)
|
||||
}
|
||||
if objType == "server" {
|
||||
gFunc = compute.Servers.GetById
|
||||
} else if objType == "host" {
|
||||
gFunc = compute.Hosts.GetById
|
||||
}
|
||||
return gFunc(us, id, jsonutils.NewDict())
|
||||
}
|
||||
|
||||
func getCommand(ctx context.Context, us *mcclient.ClientSession, ip string, port int) (string, *BaseCommand, error) {
|
||||
if !o.Options.EnableAutoLogin {
|
||||
return "", nil, nil
|
||||
}
|
||||
s := auth.GetAdminSession(ctx, o.Options.Region, "v2")
|
||||
key, err := compute.Sshkeypairs.GetById(s, userCred.GetProjectId(), jsonutils.Marshal(map[string]bool{"admin": true}))
|
||||
key, err := compute.Sshkeypairs.GetById(s, us.GetProjectId(), jsonutils.Marshal(map[string]bool{"admin": true}))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -84,7 +101,7 @@ func getCommand(ctx context.Context, userCred mcclient.TokenCredential, ip strin
|
||||
log.Warningf("try use %s without password login error: %v", user, err)
|
||||
return "", nil, nil
|
||||
} else {
|
||||
cmd = NewBaseCommand(o.Options.SshToolPath)
|
||||
cmd = NewBaseCommand(us, o.Options.SshToolPath)
|
||||
cmd.AppendArgs("-i", filename)
|
||||
cmd.AppendArgs("-q")
|
||||
cmd.AppendArgs("-o", "StrictHostKeyChecking=no")
|
||||
@@ -99,12 +116,29 @@ func getCommand(ctx context.Context, userCred mcclient.TokenCredential, ip strin
|
||||
return filename, cmd, nil
|
||||
}
|
||||
|
||||
func NewSSHtoolSolCommand(ctx context.Context, userCred mcclient.TokenCredential, ip string, query jsonutils.JSONObject) (*SSHtoolSol, error) {
|
||||
port := 22
|
||||
if query != nil {
|
||||
if _port, _ := query.Int("webconsole", "port"); _port != 0 {
|
||||
func NewSSHtoolSolCommand(ctx context.Context, us *mcclient.ClientSession, ip string, body jsonutils.JSONObject) (*SSHtoolSol, error) {
|
||||
var (
|
||||
port = 22
|
||||
objId = ""
|
||||
objType = ""
|
||||
obj jsonutils.JSONObject = nil
|
||||
)
|
||||
if body != nil {
|
||||
if _port, _ := body.Int("webconsole", "port"); _port != 0 {
|
||||
port = int(_port)
|
||||
}
|
||||
objId, _ = body.GetString("webconsole", "id")
|
||||
if objId != "" {
|
||||
objType, _ = body.GetString("webconsole", "type")
|
||||
if objType == "" {
|
||||
return nil, httperrors.NewInputParameterError("type must provided")
|
||||
}
|
||||
var err error
|
||||
obj, err = getObjectFromRemote(us, objId, objType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), time.Second*2)
|
||||
@@ -113,7 +147,7 @@ func NewSSHtoolSolCommand(ctx context.Context, userCred mcclient.TokenCredential
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
keyFile, cmd, err := getCommand(ctx, userCred, ip, port)
|
||||
keyFile, cmd, err := getCommand(ctx, us, ip, port)
|
||||
if err != nil {
|
||||
log.Errorf("getCommand error: %v", err)
|
||||
}
|
||||
@@ -128,6 +162,8 @@ func NewSSHtoolSolCommand(ctx context.Context, userCred mcclient.TokenCredential
|
||||
keyFile: keyFile,
|
||||
buffer: []byte{},
|
||||
needShowInfo: true,
|
||||
objectType: objType,
|
||||
object: obj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -225,3 +261,17 @@ func (c *SSHtoolSol) ShowInfo() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *SSHtoolSol) GetRecordObject() *recorder.Object {
|
||||
if c.object == nil {
|
||||
return nil
|
||||
}
|
||||
id, _ := c.object.GetString("id")
|
||||
name, _ := c.object.GetString("name")
|
||||
notes := map[string]interface{}{
|
||||
"user": c.username,
|
||||
"ip": c.IP,
|
||||
"port": c.Port,
|
||||
}
|
||||
return recorder.NewObject(id, name, c.objectType, jsonutils.Marshal(notes))
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
|
||||
webconsole_api "yunion.io/x/onecloud/pkg/apis/webconsole"
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
@@ -34,6 +36,7 @@ import (
|
||||
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules/k8s"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/command"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/models"
|
||||
o "yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/session"
|
||||
)
|
||||
@@ -51,6 +54,14 @@ func InitHandlers(app *appsrv.Application) {
|
||||
app.AddHandler("POST", ApiPathPrefix+"baremetal/<id>", auth.Authenticate(handleBaremetalShell))
|
||||
app.AddHandler("POST", ApiPathPrefix+"ssh/<ip>", auth.Authenticate(handleSshShell))
|
||||
app.AddHandler("POST", ApiPathPrefix+"server/<id>", auth.Authenticate(handleServerRemoteConsole))
|
||||
|
||||
for _, man := range []db.IModelManager{
|
||||
models.GetCommandLogManager(),
|
||||
} {
|
||||
db.RegisterModelManager(man)
|
||||
handler := db.NewModelHandler(man)
|
||||
dispatcher.AddModelDispatcher(ApiPathPrefix, app, handler)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchK8sEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*command.K8sEnv, error) {
|
||||
@@ -99,6 +110,7 @@ func fetchK8sEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*
|
||||
f.WriteString(conf)
|
||||
|
||||
return &command.K8sEnv{
|
||||
Session: adminSession,
|
||||
Cluster: k8sReq.Cluster,
|
||||
Namespace: k8sReq.Namespace,
|
||||
Pod: podName,
|
||||
@@ -157,13 +169,12 @@ func handleK8sLog(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func handleSshShell(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
|
||||
env, err := fetchCloudEnv(ctx, w, r)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
cmd, err := command.NewSSHtoolSolCommand(ctx, userCred, env.Params["<ip>"], env.Body)
|
||||
cmd, err := command.NewSSHtoolSolCommand(ctx, env.ClientSessin, env.Params["<ip>"], env.Body)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
@@ -189,7 +200,7 @@ func handleBaremetalShell(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
cmd, err := command.NewIpmitoolSolCommand(&info)
|
||||
cmd, err := command.NewIpmitoolSolCommand(&info, env.ClientSessin)
|
||||
if err != nil {
|
||||
httperrors.GeneralServerError(ctx, w, err)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/webconsole"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
var commandLogManager *SCommandLogManager
|
||||
|
||||
type CommandType string
|
||||
|
||||
const (
|
||||
CommandTypeSSH = "ssh"
|
||||
)
|
||||
|
||||
func InitCommandLog() {
|
||||
commandLogManager = GetCommandLogManager()
|
||||
}
|
||||
|
||||
func GetCommandLogManager() *SCommandLogManager {
|
||||
if commandLogManager != nil {
|
||||
return commandLogManager
|
||||
}
|
||||
commandLogManager = &SCommandLogManager{
|
||||
SOpsLogManager: db.SOpsLogManager{
|
||||
SModelBaseManager: db.NewModelBaseManagerWithSplitable(
|
||||
SCommandLog{},
|
||||
"command_log_tbl",
|
||||
"commandlog",
|
||||
"commandlogs",
|
||||
"id",
|
||||
"start_time",
|
||||
consts.SplitableMaxDuration(),
|
||||
consts.SplitableMaxKeepMonths(),
|
||||
),
|
||||
},
|
||||
}
|
||||
commandLogManager.SetVirtualObject(commandLogManager)
|
||||
return commandLogManager
|
||||
}
|
||||
|
||||
type SCommandLogManager struct {
|
||||
db.SOpsLogManager
|
||||
}
|
||||
|
||||
type SCommandLog struct {
|
||||
db.SOpsLog
|
||||
|
||||
SessionId string `width:"128" charset:"ascii" list:"user"`
|
||||
AccessedAt time.Time `nullable:"false" list:"user" create:"required"`
|
||||
Type string `width:"32" charset:"utf8" nullable:"true" list:"user" create:"required"`
|
||||
StartTime time.Time `list:"user" create:"required"`
|
||||
Command CommandType `charset:"utf8" list:"user" create:"required"`
|
||||
}
|
||||
|
||||
type CommandLogCreateInput struct {
|
||||
ObjId string
|
||||
ObjName string
|
||||
ObjType string
|
||||
Action string
|
||||
UserId string
|
||||
User string
|
||||
TenantId string
|
||||
Tenant string
|
||||
DomainId string
|
||||
Domain string
|
||||
ProjectDomainId string
|
||||
ProjectDomain string
|
||||
Roles string
|
||||
SessionId string
|
||||
AccessedAt time.Time
|
||||
Type CommandType
|
||||
StartTime time.Time
|
||||
Command string
|
||||
Notes jsonutils.JSONObject
|
||||
}
|
||||
|
||||
func (m *SCommandLogManager) Create(ctx context.Context, userCred mcclient.TokenCredential, input *CommandLogCreateInput) (*SCommandLog, error) {
|
||||
data := jsonutils.Marshal(input)
|
||||
obj, err := db.DoCreate(GetCommandLogManager(), ctx, userCred, jsonutils.NewDict(), data, userCred)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Create CommandLog")
|
||||
}
|
||||
return obj.(*SCommandLog), nil
|
||||
}
|
||||
|
||||
func (m *SCommandLogManager) ListItemFilter(
|
||||
ctx context.Context,
|
||||
q *sqlchemy.SQuery,
|
||||
userCred mcclient.TokenCredential,
|
||||
input api.CommandLogListInput,
|
||||
) (*sqlchemy.SQuery, error) {
|
||||
q, err := m.SOpsLogManager.ListItemFilter(ctx, q, userCred, input.OpsLogListInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "SOpsLogManager.ListItemFilter")
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package models // import "yunion.io/x/onecloud/pkg/webconsole/models"
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"yunion.io/x/log"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
)
|
||||
|
||||
func InitDB() error {
|
||||
for _, manager := range []db.IModelManager{
|
||||
/*
|
||||
* Important!!!
|
||||
* initialization order matters, do not change the order
|
||||
*/
|
||||
GetCommandLogManager(),
|
||||
} {
|
||||
err := manager.InitializeData()
|
||||
if err != nil {
|
||||
log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -23,6 +23,8 @@ var (
|
||||
type WebConsoleOptions struct {
|
||||
common_options.CommonOptions
|
||||
|
||||
common_options.DBOptions
|
||||
|
||||
//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"`
|
||||
@@ -43,5 +45,9 @@ func OnOptionsChange(oldO, newO interface{}) bool {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if common_options.OnDBOptionsChange(&oldOpts.DBOptions, &newOpts.DBOptions) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package recorder // import "yunion.io/x/onecloud/pkg/webconsole/recorder"
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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 recorder
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/models"
|
||||
)
|
||||
|
||||
type Recoder interface {
|
||||
Start()
|
||||
Write(data string)
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Id string
|
||||
Name string
|
||||
Type string
|
||||
Notes jsonutils.JSONObject
|
||||
}
|
||||
|
||||
func NewObject(id, name, oType string, notes jsonutils.JSONObject) *Object {
|
||||
return &Object{
|
||||
Id: id,
|
||||
Name: name,
|
||||
Type: oType,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
type cmdRecoder struct {
|
||||
cs *mcclient.ClientSession
|
||||
sessionId string
|
||||
accessedAt time.Time
|
||||
buffer []string
|
||||
curCmd string
|
||||
cmdCh chan string
|
||||
object *Object
|
||||
}
|
||||
|
||||
func NewCmdRecorder(s *mcclient.ClientSession, obj *Object, sessionId string, accessedAt time.Time) Recoder {
|
||||
return &cmdRecoder{
|
||||
cs: s,
|
||||
sessionId: sessionId,
|
||||
accessedAt: accessedAt,
|
||||
buffer: make([]string, 0),
|
||||
curCmd: "",
|
||||
cmdCh: make(chan string),
|
||||
object: obj,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) Write(data string) {
|
||||
if data == "\n" || data == "\r" {
|
||||
r.sendMessage(r.curCmd)
|
||||
return
|
||||
}
|
||||
r.curCmd += data
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) cleanCmd() *cmdRecoder {
|
||||
r.curCmd = ""
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) sendMessage(msg string) {
|
||||
r.cmdCh <- msg
|
||||
r.cleanCmd()
|
||||
log.Infof("Message %q sended", msg)
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) Start() {
|
||||
for {
|
||||
select {
|
||||
case cmd := <-r.cmdCh:
|
||||
if err := r.save(cmd); err != nil {
|
||||
log.Errorf("save comand %q error: %v", cmd, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) save(command string) error {
|
||||
if r.object == nil {
|
||||
return nil
|
||||
}
|
||||
userCred := r.cs.GetToken()
|
||||
input := r.newModelInput(userCred, command)
|
||||
_, err := models.GetCommandLogManager().Create(r.cs.GetContext(), userCred, input)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Create command log by input: %s", jsonutils.Marshal(input))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *cmdRecoder) newModelInput(userCred mcclient.TokenCredential, command string) *models.CommandLogCreateInput {
|
||||
return &models.CommandLogCreateInput{
|
||||
ObjId: r.object.Id,
|
||||
ObjName: r.object.Name,
|
||||
ObjType: r.object.Type,
|
||||
Notes: r.object.Notes,
|
||||
Action: "record",
|
||||
UserId: userCred.GetUserId(),
|
||||
User: userCred.GetUserName(),
|
||||
TenantId: userCred.GetTenantId(),
|
||||
Tenant: userCred.GetTenantName(),
|
||||
DomainId: userCred.GetDomainId(),
|
||||
Domain: userCred.GetDomainName(),
|
||||
ProjectDomainId: userCred.GetProjectDomainId(),
|
||||
ProjectDomain: userCred.GetProjectDomain(),
|
||||
Roles: strings.Join(userCred.GetRoles(), ","),
|
||||
SessionId: r.sessionId,
|
||||
AccessedAt: r.accessedAt,
|
||||
Type: models.CommandTypeSSH,
|
||||
StartTime: time.Now(),
|
||||
Command: command,
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ func initSocketHandler(so socketio.Socket, p *session.Pty) {
|
||||
}
|
||||
} else {
|
||||
p.Pty.Write([]byte(data))
|
||||
go p.Session.GetRecorder().Write(data)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -27,12 +27,16 @@ import (
|
||||
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/util/signalutils"
|
||||
_ "yunion.io/x/sqlchemy/backends"
|
||||
|
||||
api "yunion.io/x/onecloud/pkg/apis/webconsole"
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon"
|
||||
app_common "yunion.io/x/onecloud/pkg/cloudcommon/app"
|
||||
"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"
|
||||
)
|
||||
@@ -78,10 +82,17 @@ func registerSigTraps() {
|
||||
|
||||
func start() {
|
||||
baseOpts := &o.Options.BaseOptions
|
||||
|
||||
// commonOpts := &o.Options.CommonOptions
|
||||
app := app_common.InitApp(baseOpts, false)
|
||||
app := app_common.InitApp(baseOpts, true)
|
||||
dbOpts := &o.Options.DBOptions
|
||||
|
||||
cloudcommon.InitDB(dbOpts)
|
||||
|
||||
webconsole.InitHandlers(app)
|
||||
|
||||
db.EnsureAppSyncDB(app, dbOpts, models.InitDB)
|
||||
|
||||
root := mux.NewRouter()
|
||||
root.UseEncodedPath()
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/recorder"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,7 +45,11 @@ const (
|
||||
CLOUDPODS = api.CLOUDPODS
|
||||
)
|
||||
|
||||
type RemoteConsoleInfo cloudprovider.ServerVncOutput
|
||||
type RemoteConsoleInfo struct {
|
||||
cloudprovider.ServerVncOutput
|
||||
|
||||
s *mcclient.ClientSession
|
||||
}
|
||||
|
||||
func NewRemoteConsoleInfoByCloud(s *mcclient.ClientSession, serverId string, query jsonutils.JSONObject) (*RemoteConsoleInfo, error) {
|
||||
ret, err := modules.Servers.GetSpecific(s, serverId, "vnc", query)
|
||||
@@ -56,6 +61,7 @@ func NewRemoteConsoleInfoByCloud(s *mcclient.ClientSession, serverId string, que
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vncInfo.s = s
|
||||
|
||||
if len(vncInfo.OsName) == 0 || len(vncInfo.VncPassword) == 0 {
|
||||
metadata, err := modules.Servers.GetSpecific(s, serverId, "metadata", nil)
|
||||
@@ -111,6 +117,10 @@ func (info *RemoteConsoleInfo) ShowInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (info *RemoteConsoleInfo) GetClientSession() *mcclient.ClientSession {
|
||||
return info.s
|
||||
}
|
||||
|
||||
func (info *RemoteConsoleInfo) GetConnectParams() (string, error) {
|
||||
switch info.Protocol {
|
||||
case ALIYUN:
|
||||
@@ -191,3 +201,7 @@ func (info *RemoteConsoleInfo) getApsaraURL() (string, error) {
|
||||
}
|
||||
return info.getConnParamsURL(options.Options.ApsaraConsoleAddr, params), nil
|
||||
}
|
||||
|
||||
func (info *RemoteConsoleInfo) GetRecordObject() *recorder.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
|
||||
"yunion.io/x/onecloud/pkg/webconsole/command"
|
||||
o "yunion.io/x/onecloud/pkg/webconsole/options"
|
||||
"yunion.io/x/onecloud/pkg/webconsole/recorder"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -124,6 +125,7 @@ type SSession struct {
|
||||
AccessToken string
|
||||
AccessedAt time.Time
|
||||
duplicateHook func()
|
||||
recorder recorder.Recoder
|
||||
}
|
||||
|
||||
func (s SSession) GetConnectParams(params url.Values) (string, error) {
|
||||
@@ -165,3 +167,11 @@ func (s *SSession) Close() error {
|
||||
func (s *SSession) RegisterDuplicateHook(f func()) {
|
||||
s.duplicateHook = f
|
||||
}
|
||||
|
||||
func (s *SSession) GetRecorder() recorder.Recoder {
|
||||
if s.recorder == nil {
|
||||
s.recorder = recorder.NewCmdRecorder(s.GetClientSession(), s.GetRecordObject(), s.GetId(), s.AccessedAt)
|
||||
go s.recorder.Start()
|
||||
}
|
||||
return s.recorder
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user