Files
cloudpods/pkg/compute/models/guest_sshable.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
}