| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- // 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
- }
|