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