mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-06-08 23:45:40 +08:00
593 lines
16 KiB
Go
593 lines
16 KiB
Go
// 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"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/tristate"
|
|
"yunion.io/x/pkg/util/httputils"
|
|
"yunion.io/x/sqlchemy"
|
|
|
|
cloudproxy_api "yunion.io/x/onecloud/pkg/apis/cloudproxy"
|
|
compute_api "yunion.io/x/onecloud/pkg/apis/compute"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
|
"yunion.io/x/onecloud/pkg/compute/sshkeys"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
|
ansible_modules "yunion.io/x/onecloud/pkg/mcclient/modules/ansible"
|
|
cloudproxy_module "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
|
|
"yunion.io/x/onecloud/pkg/util/ansible"
|
|
"yunion.io/x/onecloud/pkg/util/logclient"
|
|
ssh_util "yunion.io/x/onecloud/pkg/util/ssh"
|
|
)
|
|
|
|
type GuestSshableTryData struct {
|
|
DryRun bool
|
|
|
|
User string
|
|
Host string
|
|
Port int
|
|
PrivateKey string
|
|
PublicKey string
|
|
|
|
MethodTried []compute_api.GuestSshableMethodData
|
|
}
|
|
|
|
func (tryData *GuestSshableTryData) AddMethodTried(tryMethodData compute_api.GuestSshableMethodData) {
|
|
tryData.MethodTried = append(tryData.MethodTried, tryMethodData)
|
|
}
|
|
|
|
func (tryData *GuestSshableTryData) outputJSON() jsonutils.JSONObject {
|
|
out := compute_api.GuestSshableOutput{
|
|
User: tryData.User,
|
|
PublicKey: tryData.PublicKey,
|
|
|
|
MethodTried: tryData.MethodTried,
|
|
}
|
|
outJSON := jsonutils.Marshal(out)
|
|
return outJSON
|
|
}
|
|
|
|
func (guest *SGuest) GetDetailsSshable(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
query jsonutils.JSONObject,
|
|
) (jsonutils.JSONObject, error) {
|
|
if guest.Status != compute_api.VM_RUNNING {
|
|
return nil, httperrors.NewBadRequestError("server sshable state can only be checked when in running state")
|
|
}
|
|
|
|
tryData := &GuestSshableTryData{
|
|
User: "cloudroot",
|
|
}
|
|
|
|
// - get admin key
|
|
privateKey, publicKey, err := sshkeys.GetSshAdminKeypair(ctx)
|
|
if err != nil {
|
|
return nil, httperrors.NewInternalServerError("fetch ssh private key: %v", err)
|
|
}
|
|
tryData.PrivateKey = privateKey
|
|
tryData.PublicKey = publicKey
|
|
|
|
if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
{
|
|
sshable := false
|
|
for i := range tryData.MethodTried {
|
|
if tryData.MethodTried[i].Sshable {
|
|
sshable = true
|
|
break
|
|
}
|
|
}
|
|
if _, err := db.Update(guest, func() error {
|
|
guest.SshableLastState = tristate.NewFromBool(sshable)
|
|
return nil
|
|
}); err != nil {
|
|
log.Errorf("update guest %s(%s) sshable_last_state to %v: %v", guest.Name, guest.Id, sshable, err)
|
|
}
|
|
}
|
|
|
|
logclient.AddActionLogWithContext(ctx, guest, logclient.ACT_TRYSSHABLE, nil, userCred, true)
|
|
return tryData.outputJSON(), nil
|
|
}
|
|
|
|
func (guest *SGuest) sshableTryEach(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
tryData *GuestSshableTryData,
|
|
) error {
|
|
gns, err := guest.GetNetworks("")
|
|
if err != nil {
|
|
return httperrors.NewInternalServerError("fetch network interface information: %v", err)
|
|
}
|
|
type gnInfo struct {
|
|
guestNetwork *SGuestnetwork
|
|
network *SNetwork
|
|
vpc *SVpc
|
|
}
|
|
// make sure the ssh port
|
|
var sshPort int
|
|
if tryData.Port != 0 {
|
|
sshPort = tryData.Port
|
|
} else {
|
|
sshPort = guest.GetSshPort(ctx, userCred)
|
|
}
|
|
tryData.Port = sshPort
|
|
var gnInfos []gnInfo
|
|
for i := range gns {
|
|
gn := &gns[i]
|
|
network, _ := gn.GetNetwork()
|
|
if network == nil {
|
|
continue
|
|
}
|
|
vpc, _ := network.GetVpc()
|
|
if vpc == nil {
|
|
continue
|
|
}
|
|
if vpc.Id == compute_api.DEFAULT_VPC_ID || vpc.Direct {
|
|
if ok := guest.sshableTryDefaultVPC(ctx, tryData, gn); ok {
|
|
return nil
|
|
}
|
|
} else {
|
|
gnInfos = append(gnInfos, gnInfo{
|
|
guestNetwork: gn,
|
|
network: network,
|
|
vpc: vpc,
|
|
})
|
|
}
|
|
}
|
|
|
|
// - check eip
|
|
if eip, err := guest.GetEipOrPublicIp(); err == nil && eip != nil {
|
|
if ok := guest.sshableTryEip(ctx, tryData, eip); ok {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
sess := auth.GetSession(ctx, userCred, "")
|
|
// - check existing proxy forward
|
|
proxyforwardTried := false
|
|
for i := range gnInfos {
|
|
gnInfo := &gnInfos[i]
|
|
gn := gnInfo.guestNetwork
|
|
port := sshPort
|
|
input := &cloudproxy_api.ForwardListInput{
|
|
Type: cloudproxy_api.FORWARD_TYPE_LOCAL,
|
|
RemoteAddr: gn.IpAddr,
|
|
RemotePort: &port,
|
|
Opaque: guest.Id,
|
|
}
|
|
params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
|
|
params.Set("details", jsonutils.JSONTrue)
|
|
res, err := cloudproxy_module.Forwards.List(sess, params)
|
|
if err != nil {
|
|
log.Warningf("list cloudproxy forwards: %v", err)
|
|
continue
|
|
}
|
|
proxyforwardTried = len(res.Data) != 0
|
|
for _, data := range res.Data {
|
|
var fwd cloudproxy_api.ForwardDetails
|
|
if err := data.Unmarshal(&fwd); err != nil {
|
|
log.Warningf("unmarshal cloudproxy forward list data: %v", err)
|
|
continue
|
|
}
|
|
if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if !proxyforwardTried {
|
|
// - create and use new proxy forward
|
|
fwdCreateInput := cloudproxy_api.ForwardCreateFromServerInput{
|
|
ServerId: guest.Id,
|
|
Type: cloudproxy_api.FORWARD_TYPE_LOCAL,
|
|
RemotePort: sshPort,
|
|
}
|
|
fwdCreateParams := jsonutils.Marshal(fwdCreateInput)
|
|
res, err := cloudproxy_module.Forwards.PerformClassAction(sess, "create-from-server", fwdCreateParams)
|
|
if err == nil {
|
|
var fwd cloudproxy_api.ForwardDetails
|
|
if err := res.Unmarshal(&fwd); err != nil {
|
|
log.Errorf("unmarshal fwd details: %q", res.String())
|
|
}
|
|
|
|
tmo := time.NewTimer(13 * time.Second)
|
|
tick := time.NewTicker(3 * time.Second)
|
|
out:
|
|
for {
|
|
select {
|
|
case <-tmo.C:
|
|
break out
|
|
case <-tick.C:
|
|
if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
|
|
return nil
|
|
}
|
|
case <-ctx.Done():
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
var reason string
|
|
if jce, ok := err.(*httputils.JSONClientError); ok {
|
|
reason = jce.Details
|
|
} else {
|
|
reason = err.Error()
|
|
}
|
|
tryData.AddMethodTried(compute_api.GuestSshableMethodData{
|
|
Method: compute_api.MethodProxyForward,
|
|
Reason: reason,
|
|
})
|
|
}
|
|
}
|
|
|
|
// - existing dnat rule
|
|
for i := range gnInfos {
|
|
gnInfo := &gnInfos[i]
|
|
gn := gnInfo.guestNetwork
|
|
vpc := gnInfo.vpc
|
|
|
|
natgwq := NatGatewayManager.Query().SubQuery()
|
|
q := NatDEntryManager.Query().
|
|
Equals("internal_ip", gn.IpAddr).
|
|
Equals("internal_port", sshPort).
|
|
Equals("ip_protocol", "tcp")
|
|
q = q.Join(natgwq, sqlchemy.AND(
|
|
sqlchemy.In(natgwq.Field("vpc_id"), vpc.Id),
|
|
sqlchemy.Equals(natgwq.Field("id"), q.Field("natgateway_id")),
|
|
))
|
|
|
|
var dnats []SNatDEntry
|
|
if err := db.FetchModelObjects(NatDEntryManager, q, &dnats); err != nil {
|
|
log.Warningf("query dnat to ssh service: %v", err)
|
|
continue
|
|
}
|
|
for j := range dnats {
|
|
dnat := &dnats[j]
|
|
if ok := guest.sshableTryDnat(ctx, tryData, dnat); ok {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (guest *SGuest) sshableTryDnat(
|
|
ctx context.Context,
|
|
tryData *GuestSshableTryData,
|
|
dnat *SNatDEntry,
|
|
) bool {
|
|
methodData := compute_api.GuestSshableMethodData{
|
|
Method: compute_api.MethodDNAT,
|
|
Host: dnat.ExternalIP,
|
|
Port: dnat.ExternalPort,
|
|
}
|
|
return guest.sshableTry(
|
|
ctx, tryData, methodData,
|
|
)
|
|
}
|
|
|
|
func (guest *SGuest) sshableTryForward(
|
|
ctx context.Context,
|
|
tryData *GuestSshableTryData,
|
|
fwd *cloudproxy_api.ForwardDetails,
|
|
) bool {
|
|
if fwd.BindAddr != "" && fwd.BindPort > 0 {
|
|
methodData := compute_api.GuestSshableMethodData{
|
|
Method: compute_api.MethodProxyForward,
|
|
Host: fwd.BindAddr,
|
|
Port: fwd.BindPort,
|
|
ForwardDetails: compute_api.ForwardDetails{
|
|
ProxyAgentId: fwd.ProxyAgentId,
|
|
ProxyEndpointId: fwd.ProxyEndpointId,
|
|
},
|
|
}
|
|
return guest.sshableTry(
|
|
ctx, tryData, methodData,
|
|
)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (guest *SGuest) sshableTryEip(
|
|
ctx context.Context,
|
|
tryData *GuestSshableTryData,
|
|
eip *SElasticip,
|
|
) bool {
|
|
methodData := compute_api.GuestSshableMethodData{
|
|
Method: compute_api.MethodEIP,
|
|
Host: eip.IpAddr,
|
|
Port: tryData.Port,
|
|
}
|
|
return guest.sshableTry(
|
|
ctx, tryData, methodData,
|
|
)
|
|
}
|
|
|
|
func (guest *SGuest) sshableTryDefaultVPC(
|
|
ctx context.Context,
|
|
tryData *GuestSshableTryData,
|
|
gn *SGuestnetwork,
|
|
) bool {
|
|
methodData := compute_api.GuestSshableMethodData{
|
|
Method: compute_api.MethodDirect,
|
|
Host: gn.IpAddr,
|
|
Port: tryData.Port,
|
|
}
|
|
return guest.sshableTry(
|
|
ctx, tryData, methodData,
|
|
)
|
|
}
|
|
|
|
func (guest *SGuest) sshableTry(
|
|
ctx context.Context,
|
|
tryData *GuestSshableTryData,
|
|
methodData compute_api.GuestSshableMethodData,
|
|
) bool {
|
|
if tryData.DryRun {
|
|
tryData.AddMethodTried(methodData)
|
|
return true
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
|
|
defer cancel()
|
|
conf := ssh_util.ClientConfig{
|
|
Username: tryData.User,
|
|
Host: methodData.Host,
|
|
Port: methodData.Port,
|
|
PrivateKey: tryData.PrivateKey,
|
|
}
|
|
ok := false
|
|
if client, err := conf.ConnectContext(ctx); err == nil {
|
|
defer client.Close()
|
|
methodData.Sshable = true
|
|
ok = true
|
|
} else {
|
|
methodData.Reason = err.Error()
|
|
}
|
|
tryData.AddMethodTried(methodData)
|
|
return ok
|
|
}
|
|
|
|
func (guest *SGuest) PerformHaveAgent(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input compute_api.GuestHaveAgentInput) (compute_api.GuestHaveAgentOutput, error) {
|
|
var output compute_api.GuestHaveAgentOutput
|
|
v := guest.GetMetadata(ctx, "__monitor_agent", userCred)
|
|
if v == "true" {
|
|
output.Have = true
|
|
return output, nil
|
|
}
|
|
v = guest.GetMetadata(ctx, "sys:monitor_agent", userCred)
|
|
if v == "true" {
|
|
output.Have = true
|
|
return output, nil
|
|
}
|
|
v = guest.GetMetadata(ctx, "telegraf_deployed", userCred)
|
|
if v == "true" {
|
|
output.Have = true
|
|
return output, nil
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func (guest *SGuest) PerformMakeSshable(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
query jsonutils.JSONObject,
|
|
input compute_api.GuestMakeSshableInput,
|
|
) (output compute_api.GuestMakeSshableOutput, err error) {
|
|
if guest.Status != compute_api.VM_RUNNING {
|
|
return output, httperrors.NewBadRequestError("make-sshable can only be performed when in running state")
|
|
}
|
|
|
|
if input.User == "" {
|
|
return output, httperrors.NewBadRequestError("missing username")
|
|
}
|
|
if input.PrivateKey == "" && input.Password == "" {
|
|
return output, httperrors.NewBadRequestError("private_key and password cannot both be empty")
|
|
}
|
|
|
|
_, projectPublicKey, err := sshkeys.GetSshProjectKeypair(ctx, guest.ProjectId)
|
|
if err != nil {
|
|
return output, httperrors.NewInternalServerError("fetch project public key: %v", err)
|
|
}
|
|
_, adminPublicKey, err := sshkeys.GetSshAdminKeypair(ctx)
|
|
if err != nil {
|
|
return output, httperrors.NewInternalServerError("fetch admin public key: %v", err)
|
|
}
|
|
|
|
tryData := &GuestSshableTryData{
|
|
DryRun: true,
|
|
Port: input.Port,
|
|
}
|
|
if err := guest.sshableTryEach(ctx, userCred, tryData); err != nil {
|
|
return output, httperrors.NewNotAcceptableError("searching for usable ssh address: %v", err)
|
|
} else if len(tryData.MethodTried) == 0 {
|
|
return output, httperrors.NewNotAcceptableError("no usable ssh address")
|
|
}
|
|
|
|
// storage sshport
|
|
if input.Port != 0 {
|
|
err := guest.SetSshPort(ctx, userCred, input.Port)
|
|
if err != nil {
|
|
return output, errors.Wrap(err, "unable to set sshport for guest")
|
|
}
|
|
}
|
|
host := ansible.Host{
|
|
Name: guest.Name,
|
|
}
|
|
host.SetVar("ansible_user", input.User)
|
|
host.SetVar("ansible_host", tryData.MethodTried[0].Host)
|
|
host.SetVar("ansible_port", fmt.Sprintf("%d", tryData.MethodTried[0].Port))
|
|
host.SetVar("ansible_become", "yes")
|
|
pb := &ansible.Playbook{
|
|
Inventory: ansible.Inventory{
|
|
Hosts: []ansible.Host{host},
|
|
},
|
|
Modules: []ansible.Module{
|
|
{
|
|
Name: "group",
|
|
Args: []string{
|
|
"name=cloudroot",
|
|
"state=present",
|
|
},
|
|
},
|
|
{
|
|
Name: "user",
|
|
Args: []string{
|
|
"name=cloudroot",
|
|
"state=present",
|
|
"group=cloudroot",
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized_key",
|
|
Args: []string{
|
|
"user=cloudroot",
|
|
"state=present",
|
|
fmt.Sprintf("key=%q", adminPublicKey),
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized_key",
|
|
Args: []string{
|
|
"user=cloudroot",
|
|
"state=present",
|
|
fmt.Sprintf("key=%q", projectPublicKey),
|
|
},
|
|
},
|
|
{
|
|
Name: "lineinfile",
|
|
Args: []string{
|
|
"dest=/etc/sudoers",
|
|
"state=present",
|
|
fmt.Sprintf("regexp=%q", "^cloudroot "),
|
|
fmt.Sprintf("line=%q", "cloudroot ALL=(ALL) NOPASSWD: ALL"),
|
|
fmt.Sprintf("validate=%q", "visudo -cf %s"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if input.PrivateKey != "" {
|
|
pb.PrivateKey = []byte(input.PrivateKey)
|
|
} else if input.Password != "" {
|
|
host.SetVar("ansible_password", input.Password)
|
|
}
|
|
|
|
cliSess := auth.GetSession(ctx, userCred, "")
|
|
pbId := ""
|
|
pbName := "make-sshable-" + guest.Id
|
|
pbModel, err := ansible_modules.AnsiblePlaybooks.UpdateOrCreatePbModel(
|
|
ctx, cliSess, pbId, pbName, pb,
|
|
)
|
|
if err != nil {
|
|
return output, httperrors.NewGeneralError(err)
|
|
}
|
|
|
|
logclient.AddActionLogWithContext(ctx, guest, logclient.ACT_MAKESSHABLE, nil, userCred, true)
|
|
output = compute_api.GuestMakeSshableOutput{
|
|
AnsiblePlaybookId: pbModel.Id,
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func (guest *SGuest) GetDetailsMakeSshableCmd(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
query jsonutils.JSONObject,
|
|
) (output compute_api.GuestMakeSshableCmdOutput, err error) {
|
|
_, projectPublicKey, err := sshkeys.GetSshProjectKeypair(ctx, guest.ProjectId)
|
|
if err != nil {
|
|
return output, httperrors.NewInternalServerError("fetch project public key: %v", err)
|
|
}
|
|
_, adminPublicKey, err := sshkeys.GetSshAdminKeypair(ctx)
|
|
if err != nil {
|
|
return output, httperrors.NewInternalServerError("fetch admin public key: %v", err)
|
|
}
|
|
|
|
varVals := [][2]string{
|
|
{"user", "cloudroot"},
|
|
{"adminpub", strings.TrimSpace(adminPublicKey)},
|
|
{"projpub", strings.TrimSpace(projectPublicKey)},
|
|
}
|
|
shellCmd := ""
|
|
for i := range varVals {
|
|
varVal := varVals[i]
|
|
shellCmd += fmt.Sprintf("%s=%q\n", varVal[0], varVal[1])
|
|
}
|
|
|
|
shellCmd += `
|
|
group="$user"
|
|
sshdir="/home/$user/.ssh"
|
|
keyfile="$sshdir/authorized_keys"
|
|
`
|
|
shellCmd += `
|
|
id -g "$group" &>/dev/null || groupadd "$group"
|
|
id -u "$user" &>/dev/null || useradd --create-home --gid "$group" "$user"
|
|
mkdir -p "$sshdir"
|
|
grep -q -F "$adminpub" "$keyfile" &>/dev/null || echo "$adminpub" >>"$keyfile"
|
|
grep -q -F "$projpub" "$keyfile" &>/dev/null || echo "$projpub" >>"$keyfile"
|
|
chown -R "$user:$group" "$sshdir"
|
|
chmod -R 700 "$sshdir"
|
|
chmod -R 600 "$keyfile"
|
|
|
|
if ! grep -q "^$user " /etc/sudoers; then
|
|
echo "$user ALL=(ALL) NOPASSWD: ALL" | EDITOR='tee -a' visudo
|
|
fi
|
|
`
|
|
output = compute_api.GuestMakeSshableCmdOutput{
|
|
ShellCmd: shellCmd,
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func (guest *SGuest) GetSshPort(ctx context.Context, userCred mcclient.TokenCredential) int {
|
|
portStr := guest.GetMetadata(ctx, compute_api.SSH_PORT, userCred)
|
|
if portStr == "" {
|
|
return 22
|
|
}
|
|
port, _ := strconv.Atoi(portStr)
|
|
return port
|
|
}
|
|
|
|
func (guest *SGuest) SetSshPort(ctx context.Context, userCred mcclient.TokenCredential, port int) error {
|
|
return guest.SetMetadata(ctx, compute_api.SSH_PORT, port, userCred)
|
|
}
|
|
|
|
func (guest *SGuest) PerformSetSshport(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input compute_api.GuestSetSshportInput) (jsonutils.JSONObject, error) {
|
|
if input.Port < 0 {
|
|
return nil, httperrors.NewInputParameterError("invalid port")
|
|
}
|
|
return nil, guest.SetSshPort(ctx, userCred, input.Port)
|
|
}
|
|
func (guest *SGuest) GetDetailsSshport(
|
|
ctx context.Context,
|
|
userCred mcclient.TokenCredential,
|
|
query jsonutils.JSONObject,
|
|
) (compute_api.GuestSshportOutput, error) {
|
|
port := guest.GetSshPort(ctx, userCred)
|
|
return compute_api.GuestSshportOutput{Port: port}, nil
|
|
}
|