Merge pull request #10713 from yousong/feature/yousong-make-sshable

Feature/yousong make sshable
This commit is contained in:
yunion-ci-robot
2021-04-16 13:16:48 +08:00
committed by GitHub
9 changed files with 327 additions and 51 deletions
+14
View File
@@ -36,3 +36,17 @@ type GuestSshableOutput struct {
MethodTried []GuestSshableMethodData
}
type GuestMakeSshableInput struct {
User string
PrivateKey string
Password string
}
type GuestMakeSshableOutput struct {
AnsiblePlaybookId string
}
type GuestMakeSshableCmdOutput struct {
ShellCmd string
}
+224 -7
View File
@@ -16,10 +16,13 @@ package models
import (
"context"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/tristate"
"yunion.io/x/sqlchemy"
cloudproxy_api "yunion.io/x/onecloud/pkg/apis/cloudproxy"
@@ -29,12 +32,16 @@ import (
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
mcclient_modules "yunion.io/x/onecloud/pkg/mcclient/modules"
cloudproxy_module "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
"yunion.io/x/onecloud/pkg/util/ansible"
"yunion.io/x/onecloud/pkg/util/httputils"
ssh_util "yunion.io/x/onecloud/pkg/util/ssh"
)
type GuestSshableTryData struct {
DryRun bool
User string
Host string
Port int
@@ -88,9 +95,37 @@ func (guest *SGuest) GetDetailsSshable(
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)
}
}
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 nil, httperrors.NewInternalServerError("fetch network interface information: %v", err)
return httperrors.NewInternalServerError("fetch network interface information: %v", err)
}
type gnInfo struct {
guestNetwork *SGuestnetwork
@@ -111,7 +146,7 @@ func (guest *SGuest) GetDetailsSshable(
if vpc.Id == compute_api.DEFAULT_VPC_ID {
// - vpc_id == "default"
if ok := guest.sshableTryDefaultVPC(ctx, tryData, gn); ok {
return tryData.outputJSON(), nil
return nil
}
} else {
gnInfos = append(gnInfos, gnInfo{
@@ -125,7 +160,7 @@ func (guest *SGuest) GetDetailsSshable(
// - check eip
if eip, err := guest.GetEipOrPublicIp(); err == nil && eip != nil {
if ok := guest.sshableTryEip(ctx, tryData, eip); ok {
return tryData.outputJSON(), nil
return nil
}
}
@@ -157,7 +192,7 @@ func (guest *SGuest) GetDetailsSshable(
continue
}
if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
return tryData.outputJSON(), nil
return nil
}
}
}
@@ -174,7 +209,7 @@ func (guest *SGuest) GetDetailsSshable(
var fwd cloudproxy_api.ForwardDetails
if err := res.Unmarshal(&fwd); err == nil {
if ok := guest.sshableTryForward(ctx, tryData, &fwd); ok {
return tryData.outputJSON(), nil
return nil
}
}
} else {
@@ -215,12 +250,12 @@ func (guest *SGuest) GetDetailsSshable(
for j := range dnats {
dnat := &dnats[j]
if ok := guest.sshableTryDnat(ctx, tryData, dnat); ok {
return tryData.outputJSON(), nil
return nil
}
}
}
return tryData.outputJSON(), nil
return nil
}
func (guest *SGuest) sshableTryDnat(
@@ -291,6 +326,11 @@ func (guest *SGuest) sshableTry(
tryData *GuestSshableTryData,
methodData compute_api.GuestSshableMethodData,
) bool {
if tryData.DryRun {
tryData.AddMethodTried(methodData)
return true
}
ctx, _ = context.WithTimeout(ctx, 7*time.Second)
conf := ssh_util.ClientConfig{
Username: tryData.User,
@@ -309,3 +349,180 @@ func (guest *SGuest) sshableTry(
tryData.AddMethodTried(methodData)
return ok
}
func (guest *SGuest) AllowPerformMakeSshable(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
) bool {
return db.IsProjectAllowGetSpec(userCred, guest, "make-sshable")
}
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,
}
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")
}
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.Name
pbModel, err := mcclient_modules.AnsiblePlaybooks.UpdateOrCreatePbModel(
ctx, cliSess, pbId, pbName, pb,
)
if err != nil {
return output, httperrors.NewGeneralError(err)
}
output = compute_api.GuestMakeSshableOutput{
AnsiblePlaybookId: pbModel.Id,
}
return output, nil
}
func (guest *SGuest) AllowGetDetailsMakeSshableCmd(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
) bool {
return db.IsProjectAllowGetSpec(userCred, guest, "make-sshable-cmd")
}
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{
[2]string{"user", "cloudroot"},
[2]string{"adminpub", strings.TrimSpace(adminPublicKey)},
[2]string{"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"
`
shellCmd += `
id -g "$group" &>/dev/null || groupadd "$group"
id -u "$user" &>/dev/null || useradd --create-home --gid "$group" "$user"
mkdir -p "$sshdir"
echo "$adminpub" >>"$sshdir/authorized_keys"
echo "$projpub" >>"$sshdir/authorized_keys"
chown -R "$user:$group" "$sshdir"
chmod -R 700 "$sshdir"
chmod -R 600 "$sshdir/authorized_keys"
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
}
+2
View File
@@ -151,6 +151,8 @@ type SGuest struct {
// 套餐名称
InstanceType string `width:"64" charset:"utf8" nullable:"true" list:"user" create:"optional"`
SshableLastState tristate.TriState `nullable:"false" default:"false" list:"user"`
}
func (manager *SGuestManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
@@ -27,7 +27,6 @@ import (
"yunion.io/x/pkg/util/regutils"
"yunion.io/x/pkg/utils"
ansible_apis "yunion.io/x/onecloud/pkg/apis/ansible"
compute_apis "yunion.io/x/onecloud/pkg/apis/compute"
identity_apis "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
@@ -372,47 +371,10 @@ func (lbagent *SLoadbalancerAgent) updateOrCreatePbModel(ctx context.Context,
pb *ansible.Playbook,
) (*mcclient_models.AnsiblePlaybook, error) {
cliSess := auth.GetSession(ctx, userCred, "", "")
if pbId == "" {
pbJson, err := mcclient_modules.AnsiblePlaybooks.Get(cliSess, pbName, nil)
if err == nil {
pbModel := &mcclient_models.AnsiblePlaybook{}
if err := pbJson.Unmarshal(pbModel); err == nil {
pbId = pbModel.Id
}
}
}
var pbJson jsonutils.JSONObject
if pbId != "" {
var err error
ansiblePbInput := &ansible_apis.AnsiblePlaybookUpdateInput{
Name: pbName,
Playbook: *pb,
}
params := ansiblePbInput.JSON(ansiblePbInput)
pbJson, err = mcclient_modules.AnsiblePlaybooks.Update(cliSess, pbId, params)
if err != nil {
return nil, errors.WithMessage(err, "update ansibleplaybook")
}
} else {
var err error
ansiblePbInput := &ansible_apis.AnsiblePlaybookCreateInput{
Name: pbName,
Playbook: *pb,
}
params := ansiblePbInput.JSON(ansiblePbInput)
pbJson, err = mcclient_modules.AnsiblePlaybooks.Create(cliSess, params)
if err != nil {
return nil, errors.WithMessage(err, "create ansibleplaybook")
}
}
pbModel := &mcclient_models.AnsiblePlaybook{}
if err := pbJson.Unmarshal(pbModel); err != nil {
return nil, errors.WithMessage(err, "unmarshal ansibleplaybook")
}
return pbModel, nil
pbModel, err := mcclient_modules.AnsiblePlaybooks.UpdateOrCreatePbModel(
ctx, cliSess, pbId, pbName, pb,
)
return pbModel, err
}
func (lbagent *SLoadbalancerAgent) PerformUndeploy(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
+61 -1
View File
@@ -14,7 +14,18 @@
package modules
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
import (
"context"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
ansible_apis "yunion.io/x/onecloud/pkg/apis/ansible"
"yunion.io/x/onecloud/pkg/mcclient"
mcclient_models "yunion.io/x/onecloud/pkg/mcclient/models"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/util/ansible"
)
type AnsiblePlaybookManager struct {
modulebase.ResourceManager
@@ -41,3 +52,52 @@ func init() {
}
registerV2(&AnsiblePlaybooks)
}
func (man *AnsiblePlaybookManager) UpdateOrCreatePbModel(
ctx context.Context,
cliSess *mcclient.ClientSession,
pbId string,
pbName string,
pb *ansible.Playbook,
) (*mcclient_models.AnsiblePlaybook, error) {
if pbId == "" {
pbJson, err := man.Get(cliSess, pbName, nil)
if err == nil {
pbModel := &mcclient_models.AnsiblePlaybook{}
if err := pbJson.Unmarshal(pbModel); err == nil {
pbId = pbModel.Id
}
}
}
var pbJson jsonutils.JSONObject
if pbId != "" {
var err error
ansiblePbInput := &ansible_apis.AnsiblePlaybookUpdateInput{
Name: pbName,
Playbook: *pb,
}
params := ansiblePbInput.JSON(ansiblePbInput)
pbJson, err = man.Update(cliSess, pbId, params)
if err != nil {
return nil, errors.Wrap(err, "update ansibleplaybook")
}
} else {
var err error
ansiblePbInput := &ansible_apis.AnsiblePlaybookCreateInput{
Name: pbName,
Playbook: *pb,
}
params := ansiblePbInput.JSON(ansiblePbInput)
pbJson, err = man.Create(cliSess, params)
if err != nil {
return nil, errors.Wrap(err, "create ansibleplaybook")
}
}
pbModel := &mcclient_models.AnsiblePlaybook{}
if err := pbJson.Unmarshal(pbModel); err != nil {
return nil, errors.Wrap(err, "unmarshal ansibleplaybook")
}
return pbModel, nil
}
+18
View File
@@ -1089,3 +1089,21 @@ type ServerCreateEipOptions struct {
func (opts *ServerCreateEipOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.Marshal(opts), nil
}
type ServerMakeSshableOptions struct {
BaseIdOptions
User string `help:"ssh username for ssh connection" default:"root"`
PrivateKey string `help:"ssh privatekey for ssh connection"`
Password string `help:"ssh password for ssh connection"`
}
func (opts *ServerMakeSshableOptions) Params() (jsonutils.JSONObject, error) {
if opts.User == "" {
return nil, fmt.Errorf("ssh username must be set")
}
if opts.PrivateKey == "" && opts.Password == "" {
return nil, fmt.Errorf("either --private-key or --password must be set")
}
return jsonutils.Marshal(opts), nil
}